补题

· · 个人记录

DP

线性 DP

P9753 [CSP-S 2023] 消消乐

遇到子段,考虑子段划分模型。

不难想到 $f[i]$ 可由前面得出的子串加上一个单位可消除子串得到。 这样的话,可以每次 $O(n^2)$ 暴力找以当前位置为结尾的单位可消除子串,设为 $s[j,i]$,则有 $$f[i] = f[j - 1] + 1$$ 左右分别对应: 1. 将之前的串延长。 2. 当前新开一串。 时间复杂度 $O(n^3)$,期望得分 $35$。 考虑优化。 由于 $s[j,i]$ 是单位可消除子串,易知 $s[j] = s[i]$。 考虑 $s[j,i]$ 还包含若干个更小的可消除子串,所以不难证明 $j$ 应该是跳过这些更小的可消除子串后得到的使得 $s[j] = s[i]$ 成立的第一个 $j$。 用 $g[i]$ 表示最大的能使 $s[g[i],i]$ 成为可消除子串的起点位置,则初始时 $g[i] = i - 1$,然后 $$g[i] \leftarrow g[g[i]] - 1$$ 直到 $s[g[i]] = s[i] \lor g[i] = 0$。 则有 $$f[i] = f[g[i] - 1] + 1 $$ 考虑对于序列中的每种不同字符,从它在序列中出现的位置至多向前跳 $n$ 次就能找到相同的字符或跳到 $0$,故时间复杂度 $O(n|W|)$。 答案即为 $\sum_{i = 1}^n f[i]$。 空间复杂度 $O(n)$。 下面考虑进一步优化。 注意到对于每个字符我们都要往前跳不少次,而最重要的是其中很多次都是其他字符。 因此我们考虑用一种分类方法把不同字符分开。 如果令 $g[i] \leftarrow g[i] - 1$,会发现跳的方式形成若干条链。 考虑在每个位置记录 $a[c][i] = g[i]$,表示对于字符 $c$ 来说,可以合并成一段的新的单位可消除子串起点。 则 $s[a[c][i]] = c$ 且 $s[a[s[i]][i],i]$ 为合法子串。 每次修改 $a[s[i]][i]$。可以用 $to[i]$ 表示该链链头,每次修改 $a[s[i]][to[i]]$ 即可。 时间复杂度 $O(n)$,空间复杂度 $O(n|W|)$。 具体实现: ```cpp for(int i = 1;i <= n;i++){ to[i] = i; int g = a[s[i] - 'a'][to[i - 1]]; if(g) to[i] = to[g - 1],f[i] = f[g - 1] + 1; // 存在链头 a[s[i] - 'a'][to[i]] = i,ans += f[i]; // 更新 a,让下一个位置接上自己 } ``` [P11233 [CSP-S 2024] 染色](https://www.luogu.com.cn/problem/P11233) 不难想到的是子序列划分模型。 $O(n^3)$ 的 DP: $f[i][x][y]$ 表示对于前 $i$ 个位置,上一个染成红色的是 $x$,上一个染成蓝色的是 $y$,此时得分的最大可能值。 那么有转移(由于每次都读取上一行的数据,这里压掉第一维): $$f[x][i] = \max_{x = 0}^{i - 2}( f[x][i - 1] + [a[i] = a[i - 1]] \times a[i]) $$ $$ f[i][x] = \max_{x = 0}^{i - 2}( f[i - 1][x] + [a[i] = a[i - 1]] \times a[i]) $$ $$ f[i - 1][i] = \max_{x = 0}^{i - 2}( f[i - 1][x] + [a[i] = a[x]] \times a[i]) $$ $$ f[i][i - 1] = \max_{x = 0}^{i - 2}( f[x][i - 1] + ]a[i] = a[x]] \times a[i]) $$ 发现它是 $O(n^2)$ 的,喜提 $50pts$。 由于我太菜了,不能直接从转移或者问题的性质中得出优化,所以考虑输出 $f[i][j]$ 找规律优化。 观察由大样例第一个数据得出的表($18$ 为输出的答案): ``` 18 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 5 10 10 10 10 10 10 0 0 0 0 0 0 0 0 5 0 5 5 5 5 5 5 5 5 5 5 5 5 5 5 10 5 0 10 10 10 10 10 5 5 5 5 5 5 5 5 10 5 10 0 15 15 15 15 5 5 5 5 5 5 5 5 10 5 10 15 0 15 15 15 5 5 5 5 5 5 5 5 10 5 10 15 15 0 15 15 5 5 5 5 5 5 5 5 10 5 10 15 15 15 0 18 5 5 5 5 5 5 5 5 10 5 10 15 15 15 18 0 ``` 容易发现蓝色与红色是对称的,所以我们先想办法把一半优化掉。 考虑使 $f[i][j],i < j$。 我们再画个转移图: ![](https://cdn.luogu.com.cn/upload/image_hosting/sjpktsbw.png) 考虑我们上面得到的 $f[i - 1][i]$ 的转移会跨越对角线,而又由对称性可知 $f[i - 1][x] = f[x][i - 1]$,所以这个转移可以被改成不跨越对角线的。 那么有: $$f[x][i] = \max_{x = 0}^{i - 2}( f[x][i - 1] + [a[i] = a[i - 1]] \times a[i]) $$ $$ f[i - 1][i] = \max_{x = 0}^{i - 2}( f[x][i - 1] + [a[i] = a[x]] \times a[i]) $$ 现在得出的表: ``` 18 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 5 10 10 10 10 10 10 0 0 0 0 0 0 0 0 0 0 5 5 5 5 5 5 0 0 0 0 0 0 0 0 0 0 0 10 10 10 10 10 0 0 0 0 0 0 0 0 0 0 0 0 15 15 15 15 0 0 0 0 0 0 0 0 0 0 0 0 0 15 15 15 0 0 0 0 0 0 0 0 0 0 0 0 0 0 15 15 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ``` 继续集中注意力,注意到除非 $a[i] = a[i - 1]$,否则答案好像总是在对角线旁边?(猜想) 继续注意。发现从上面的表中向右走等价于 $i$ 和 $i - 1$ 被染为同色。 注意到向右走的答案会更大当且仅当 $a[i] = a[i - 1]$。所以,猜想正确。 考虑不存在长度 $\ge 2$ 的相同 $a[i]$ 段的情况,对于所有 $i$,它的答案都在对角线旁边。 先处理不存在长度 $\ge 2$ 的相同 $a[i]$ 段的情况。考虑如何写出代码。 再观察一遍转移图: ![qwq](https://cdn.luogu.com.cn/upload/image_hosting/sjpktsbw.png) 显然,在没有长度 $\ge 2$ 相同段的情况下,有 $$f[i][j] = f[i][j + 1] \ (j > i)$$ 而每一次 $f[i][i + 1]$ 的转移只与 $f[j][i] \ (0 \le j < i)$ 有关。 联立得,每一次 $f[i][i + 1]$ 的转移只与 $f[j][j + 1] \ (0 \le j < i)$ 有关。 此时如何优化已经显而易见。 用 $f[i]$ 表示上面的 $f[i][i + 1]$。 由之前得出的转移: $$ f[i - 1][i] = \max_{x = 0}^{i - 2}( f[x][i - 1] + [a[i] = a[x]] \times a[i]) $$ 则在上述情况下, $$f[i - 1] = \max_{x = 0}^{i - 2} (f[x] + [a[i] = a[x]] \times a[i]) \ (1 \le i \le n)$$ 阶段性成果: ```cpp #include <bits/extc++.h> using namespace std; typedef long long ll; const int MAXN = 2020; int n,a[MAXN]; ll f[MAXN]; void init() { memset(f,0,sizeof f); cin >> n; for(int i = 1;i <= n;i++) cin >> a[i]; } void solve() { init(); for(int i = 1;i <= n;i++){ for(int x = 0;x <= i - 2;x++){ f[i - 1] = max(f[i - 1],f[x] + (a[i] == a[x]) * a[i]); } } cout << *max_element(f,f + n) << '\n'; } int main(){ int t; cin >> t; while(t--) solve(); } ``` 再考虑存在长度 $\ge 2$ 的相同 $a[i]$ 段的情况。 回忆我们刚刚干了什么,发现我们事实上做了: $$f[i] \leftarrow f[i][j] \ (i < j \le n) $$ 考虑这样的段,发现如果 $a[i] = a[i - 1]$,则由上面打的 $f$ 表发现: $$f[i][j + 1] \leftarrow f[i][j] + a[i] \ (i < j < n) $$ 对应到我们现在定义的状态,则有 $$f[j] \leftarrow f[j] + a[i] \ (0 \le j < i - 1) $$ 暴力加即可。 做完这些,我们得到了一个全新的,时间 $O(n^2)$,空间 $O(n)$ 的算法,仍然 $50pts$。 阶段性成果: ```cpp #include <bits/extc++.h> using namespace std; typedef long long ll; const int MAXN = 2e5 + 10; int n,a[MAXN]; ll f[MAXN]; void init() { cin >> n; memset(f,0,sizeof(ll) * (n + 7)); for(int i = 1;i <= n;i++) cin >> a[i]; } void solve() { init(); for(int i = 1;i <= n;i++){ for(int x = 0;x <= i - 2;x++){ f[i - 1] = max(f[i - 1],f[x] + (a[i] == a[x]) * a[i]); } if(a[i] == a[i - 1]){ for(int j = 0;j <= i - 2;j++) f[j] += a[i]; } } cout << *max_element(f,f + n) << '\n'; } int main(){ int t; cin >> t; while(t--) solve(); } ``` 继续优化。 观察上述转移方程,分类讨论发现: $$f[i - 1] = \max(\max_{1 \le j \le i - 2} f[j] ,\max_{1 \le j \le i - 2,a[j] = a[i]}f[j] + a[i])$$ 因此考虑记录 $f[j]$ 的最大值和 $a[j] = C$ 的 $f[j]$ 最大值(开桶记录),直接转移即可。 做完这些,我们得到了一个全新的,时间 $O(n^2)$,空间 $O(n)$ 的算法,但是常数比上一个小一点,喜提 $60pts$。 阶段性成果: ```cpp #include <bits/extc++.h> using namespace std; typedef long long ll; const int MAXN = 2e5 + 10,MAXV = 1e6 + 10; int n,a[MAXN]; ll f[MAXN],premax,maxlst[MAXV]; void init() { cin >> n; memset(f,0,sizeof(ll) * (n + 7)); premax = 0; for(int i = 1;i <= n;i++) cin >> a[i],maxlst[a[i]] = 0; } void solve() { init(); for(int i = 2;i <= n;i++){ f[i - 1] = max(premax,maxlst[a[i]] ? f[maxlst[a[i]]] + a[i] : 0); if(a[i] == a[i - 1]) { premax += a[i]; for(int j = 0;j <= i - 2;j++) { f[j] += a[i]; } } premax = max(premax,f[i - 1]); if(maxlst[a[i - 1]] == 0 || f[i - 1] > f[maxlst[a[i - 1]]]) maxlst[a[i - 1]] = i - 1; } cout << *max_element(f,f + n) << '\n'; } int main(){ int t; cin >> t; while(t--) solve(); } ``` 不难发现要实现前缀加,单点查,所以直接糊一个 BIT 上去就完事了!!!11。 时间复杂度 $O(n \log n)$,空间复杂度 $O(n + V)$。 注意 BIT 下标必须从 $1$ 开始,所以我们把所有下标 $ + 1$。 由于没有把修改直接应用于 $f$,所以输出答案时要输出 $premax$ 而不是 $\max f[i]$。 ```cpp #include <bits/extc++.h> using namespace std; typedef long long ll; const int MAXN = 2e5 + 10,MAXV = 1e6 + 10; int n,a[MAXN]; ll f[MAXN],premax,maxlst[MAXV],ans; struct bit{ ll t[MAXN]; void init(){ memset(t,0,sizeof(ll) * (n + 7)); } int lb(int x){ return x & -x; } void add(int loc,ll val){ for(int i = loc;i <= n;i += lb(i)) t[i] += val; } void add(int l,int r,ll val){ add(l,val),add(r + 1,-val); } ll query(int loc){ ll res = 0; for(int i = loc;i;i -= lb(i)) res += t[i]; return res; } } t; void init() { cin >> n; memset(f,0,sizeof(ll) * (n + 7)); t.init(); premax = ans = 0; for(int i = 1;i <= n;i++) cin >> a[i],maxlst[a[i]] = 0; } void solve() { init(); for(int i = 2;i <= n;i++){ f[i - 1] = max(premax,maxlst[a[i]] ? f[maxlst[a[i]]] + t.query(maxlst[a[i]] + 1) + a[i] : 0); if(a[i] == a[i - 1]) { premax += a[i]; t.add(1,i - 1,a[i]); } premax = max(premax,f[i - 1]); if(maxlst[a[i - 1]] == 0 || f[i - 1] > f[maxlst[a[i - 1]]] + t.query(maxlst[a[i - 1]] + 1)) maxlst[a[i - 1]] = i - 1; } cout << premax << '\n'; } int main(){ int t; cin >> t; while(t--) solve(); } ``` # 图论 ## 图染色 [P9869 [NOIP2023] 三值逻辑](https://www.luogu.com.cn/problem/P9869) 容易想到的是先求出原序列和末序列,末序列中的值为对应值在原序列中的下标,然后这原值和末值应相等。 可能有人和我一样想到用一个 $fa[u]$ 数组表示末序列,然后再用另一个数组 $val[u]$ 记录一个点是否是 $T,F,U$。 这个做法的局限性在于 $val[u]$ 记录的值具有时效性。 第一种情况是我们在 $+$ 操作时 $val[u] \leftarrow val[fa[v]]$。 例如下面一组数据: ``` 3 3 + 3 1 U 1 + 1 3 ``` 根据上面的方法,我们认为最终 $fa[1] = 1,val[1] = U$。显然,这是错的。 因为第三个操作时,我们读取了 $val[3]$,而 $val[3]$ 是在我们操作二之前得到的 $val[1]$。 然而我们在第三个操作时会错误地认为 $val[3] = val[1] = U$。 我们改一下,在 $+$ 操作时 $val[u] \leftarrow val[v]$。这样,每次读取的都是对应变量的当前值,是正确的。 此外,对于 $u$ 与 $fa[u]$ 的 $+/-$ 关系,我们还应用一个 $con[u]$ 数组来记录。当每次更新 $fa[u] \leftarrow fa[v]$ 时,还应更新 $con[u] \leftarrow [sgn = -] \oplus con[v]$。 然后建图染色即可。这样,本题就做完了。 其实本题的主要难度并不在于后面的图染色/并查集做法,而是在于保持头脑清醒并理解如何记录关系的变换。 # DS ## DSU