## 矩阵最大值 #模拟 首先我们可以通过 $\mathcal O(nm)$ 的复杂度预处理出每一行和每一列的最大值,然后求出初始时一共有多少个满足条件的元素。 现在考虑修改对答案的影响。注意题目中加粗强调了修改之后的元素一定比原来更大,这也就代表着原先是最小值的元素再修改之后不会失去其地位,相当于提示我们仅需维护最大位置即可。 1. 修改后第 $x$ 行的最大值发生改变:需要减去第 $x$ 行原先的答案,更新最大值下标数组。 2. 修改后第 $y$ 列的最大值发生改变:需要减去第 $y$ 列原先的答案,更新最大值下标数组。 然后是对答案的增加。判断第 $x$ 行的最大位置是否为 $y$ 以及第 $y$ 列的最大位置是否为 $x$ 即可。然后你就会发现样例挂了。 仔细思考,如果第 $x$ 行和第 $y$ 列的最大位置没有发生任何改变,那么最后一步的增量是完全无需的,因为我们根本就没有减掉原来的贡献(如果你要减的话面临减重的问题,同样需要额外进行操作)。所以开个变量记录是否发生更改即可。 时间复杂度 $\mathcal O(nm+q)$。 ```cpp #include <fstream> #include <vector> using namespace std; using LL = long long; const LL kMaxN = 2e5 + 5; ifstream cin("matrix.in"); ofstream cout("matrix.out"); LL a[kMaxN], b[kMaxN], n, m, q, ans; // 判断第 x 行 bool check(LL x) { return b[a[x]] == x; } bool check2(LL x) { return a[b[x]] == x; } int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> m >> q; vector<vector<LL>> v(n + 1, vector<LL>(m + 1)); for (LL i = 1; i <= n; i++) { for (LL j = 1; j <= m; j++) { cin >> v[i][j]; } } for (LL i = 1; i <= n; i++) { for (LL j = 1; j <= m; j++) { if (v[i][j] > v[i][a[i]]) { a[i] = j; } } } for (LL j = 1; j <= m; j++) { for (LL i = 1; i <= n; i++) { if (v[i][j] > v[b[j]][j]) { b[j] = i; } } } for (LL i = 1; i <= n; i++) { ans += check(i); } for (LL x, y, t; q; q--) { cin >> x >> y >> t; bool change = 0; if (a[x] != y && t > v[x][a[x]]) { ans -= check(x); change = 1; a[x] = y; } if (b[y] != x && t > v[b[y]][y]) { ans -= check2(y); change = 1; b[y] = x; } v[x][y] = t; ans += change && a[x] == y && b[y] == x; cout << ans << '\n'; } return 0; } ``` ## 病毒感染 #模拟 本题我赛时思路跟托石一样,根本无法回忆或复述,所以讲不了赛时思路。 - [!] 需要注意的是,提供的矩阵是对于细胞而言对病毒的易感程度,而非对于被特定病毒感染的细胞对病毒的易感程度。这直接导致了我最开始的思路完全错误。 首先是对于 $p=1$,枚举可能的答案病毒类型 $i$,如果初始感染了病毒 $i$ 的细胞 $i$ 可以被任意一种其它的病毒感染的话,那么病毒 $i$ 将不复存在。可以直接 $\mathcal O(n^2)$ 或 $\mathcal O(n^3)$ 判断。 当然,我赛时代码也就过了这些可怜的 $40$ 分;对于 $p=2$,我完全没有得到任何分数。可恶的捆绑测试,我与你不共戴天。 对于 $p=2$,我们还是枚举病毒 $i$,判断病毒 $i$ 能否在某种攻击顺序当中存活下来。直接判断 $i$ 是否会被其它病毒给干死,然后 $i$ 能否干死可以把 $i$ 干死的病毒是不正确的,因为 $i$ 可能提前感染其它类型的病毒,以此留下后代。$i$ 身已死,病毒永存。 为了快速判断病毒 $i$ 和病毒 $j$ 谁在某个细胞的易感程度更高,可以令 $id_{i,a_{i,j}}=j$,这样即可 $\mathcal O(1)$ 判断。 枚举病毒 $i$ 后,再次枚举细胞 $j$,判断 $i$ 能否在任意一个 $j$ 上留下后代。具体来说,$i$ 的优先级要比在 $j$ 最开始时自带的 $j$ 病毒的优先级要高;其次,定义门槛值 ${} zrr_i {}$ 表示细胞 $i$ 对于所有其排列中 ${} j\ge zrr_i {}$ 的病毒 $a_j$ 都可能会感染掉 $i$,需要满足 $i$ 在 $j$ 的排名不能超过 $j$ 的门槛值,这样 $j$ 就不会被其它病毒感染了。 至于如何求 $zrr$ 数组,可以使用 $\mathcal O(n^3)$ 暴力计算。枚举病毒 $i$,枚举细胞 $j$ 和病毒 $k$,如果 $k$ 可以感染最开始的细胞 $j$,那么求 $\max id_{i,k}$ 即 $k$ 在 $i$ 中的排名,然后对这个最大值求 $\min$,得到可以感染 $i$ 的最小病毒,也就是门槛值。 ```cpp #include <fstream> #include <vector> using namespace std; const int kMaxN = 505; ifstream cin("virus.in"); ofstream cout("virus.out"); int a[kMaxN][kMaxN], id[kMaxN][kMaxN], zrr[kMaxN], n, p; vector<int> ans; int main() { cin >> n; for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { cin >> a[i][j]; id[i][a[i][j]] = j; } } cin >> p; if (p == 1) { for (int i = 1; i <= n; i++) { if (id[i][i] == 1) { ans.push_back(i); } } } else { for (int i = 1; i <= n; i++) { zrr[i] = n; for (int j = 1; j <= n; j++) { int jb = 0; for (int k = 1; k <= n; k++) { if (id[j][k] <= id[j][j]) { jb = max(jb, id[i][k]); } } zrr[i] = min(zrr[i], jb); } } for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (zrr[j] >= id[j][i] && id[j][i] <= id[j][j]) { ans.push_back(i); break; } } } } cout << ans.size() << '\n'; for (int i : ans) { cout << i << ' '; } return 0; } ``` ## 怪物猎人 #背包dp #树形dp 考虑树形 DP。首先我们可以通过 $\mathcal O(n)$ 的复杂度快速计算出初始时的答案。 发现 $n\le 2000$,考虑将杀死的节点数量加入状态。设 $dp_{i,j}$ 表示在 $i$ 的子树当中杀死了 $j$ 个节点,可以对答案减少的最大值。但是会发现一个问题,既然我们杀死节点 $i$ 会对答案减少 $2\times a_i$($i=1$ 除外)和 $\sum\limits_{j\in\operatorname{son}(i)}a_j$,那么如果 $i$ 和其儿子 $j$ 都被杀死了,儿子 $j$ 减少的 $a_i$ 不应该乘 $2$,因为 $i$ 已经杀死了不需要求儿子的 $a_j$ 和,但是如果不加当 $i$ 没被杀死时仍然需要求儿子的 $a_j$ 和。 考虑增加状态。设 $dp_{i,j,0/1}$ 表示在 $i$ 的子树当中杀死了 $j$ 个节点,其中 $i$ 是否($1/0$)被删除,所能对答案减少的最大值。这是一个 $0/1$ 背包问题,枚举当前节点的杀死节点数,然后对于所有儿子 $j$ 枚举其杀死节点数,最后转移(注意内外循环不能反,否则会导致同一个儿子 $j$ 的不同杀死数反复进入背包)。 直接转移是 $\mathcal O(n^3)$。不过令 ${} sum_i {}$ 表示 $i$ 在枚举儿子 $j$ 之前的子树和,$siz_j$ 表示 $j$ 的子树大小,转移时只枚举 $sum_i\times siz_j$,做扩散型,它的复杂度是 $\mathcal O(n^2)$ 的。 一定要注意,如果先把 $siz_j$ 加入到 $sum_i$ 后转移,做收集型,它的时间复杂度会退化。我就是因为这个原因导致 TLE 挂 $80$ 分。 正确转移的时间复杂度证明如下: $ \begin{aligned} &\textcolor{white}{=}\sum_{i=1}^n\sum_{j=1}^{|son_i|}\left(\sum_{k=1}^{j-1}siz_{son_{i,k}}\right)\times siz_{siz_{i,j}} \\ &=\sum_{i=1}^n\sum_{1\le j<k\le |son_i|}siz_{son_{i,j}}\times siz_{son_{i,k}} \\ \end{aligned} $ 即求和对于 $i$, $ siz_i^2-\sum_{j\in son_i}siz_{j}^2 $ 每个 $siz_i^2$ 都会加上一次,减去一次;除了 $siz_1$ 以外,因为没有节点的儿子是它。所以时间复杂度为 $\mathcal O\left(siz_1^2\right)$,即 $\mathcal O\left(n^2\right)$。 然后是我的那个算法,只是 $k$ 的枚举上界来到了 $j$ 而非 $j-1$,但是导致对于每个 $i$ 都额外增加了 $\mathcal O\left(\sum\limits_{j\in son_i}siz_j^2\right)$ 的时间复杂度,即最坏 $\mathcal O(n^3)$。 ```cpp #include <fstream> #include <vector> using namespace std; using LL = long long; const LL kMaxN = 2005; ifstream cin("monster.in"); ofstream cout("monster.out"); LL a[kMaxN], dp[kMaxN], sub[kMaxN][kMaxN][2], siz[kMaxN], n, mx; vector<LL> e[kMaxN]; void dfs(LL x, LL f) { LL sum = a[x]; for (LL i : e[x]) { if (i == f) { continue; } dfs(i, x); dp[x] += dp[i]; sum += a[i]; } dp[x] += sum; } void solve(LL x, LL f) { siz[x] = 1; LL sum = a[x]; for (LL i : e[x]) { if (i != f) { solve(i, x); sum += a[i]; } } sub[x][1][1] = (x != 1) * a[x] + sum; // 只删了自己 for (LL i : e[x]) { if (i == f) { continue; } for (LL k = siz[x]; k >= 0; k--) { for (LL j = 0; j <= siz[i]; j++) { sub[x][k + j][0] = max(sub[x][k + j][0], sub[x][k][0] + max(sub[i][j][0], sub[i][j][1])); // 自己不删,儿子删不删都不变 sub[x][k + j][1] = max(sub[x][k + j][1], sub[x][k][1] + max(sub[i][j][0], sub[i][j][1] - a[i])); // 自己删了,儿子没有删不影响,儿子删了就少一个儿子的贡献 } } siz[x] += siz[i]; } } int main() { cin.tie(0)->sync_with_stdio(0); cin >> n; for (LL i = 1, u, v; i < n; i++) { cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } for (LL i = 1; i <= n; i++) { cin >> a[i]; } dfs(1, 0); solve(1, 0); cout << dp[1] << ' '; for (LL i = 1; i <= n; i++) { cout << dp[1] - max(sub[1][i][0], sub[1][i][1]) << ' '; } return 0; } ``` ## 集卡游戏 #单调栈 #线段树 我采用的是暴力。 暴力枚举在第一个数列当中选择的区间,那么对于后面的数列,有些位置是被禁用的,它们将数列分成了若干块,求每一块的和的最大值即可。时间复杂度 $\mathcal O(n^2m)$,直接通过 $40$ 分。