## LIS #优先队列 #序列dp 首先注意到每次从 $i$ 跳到 $j$ 其中的 $j$ 都会是最小的一个,因此不难维护一个优先队列,按照 $a_i$ 从小到大排序,附加维护编号 $i$。从前往后依次遍历每一个元素 $a_i$,规定优先队列里面维护的是所有 $a_j$ $(j<i)$ 的还没有进行没有转移的元素,此时我们在优先队列头依次取出若干个满足 $a_j<a_i$ 的元素,进行 $j\rightarrow i$ 的转移。那么问题来了,如何进行转移呢? 维护两个值 $f_1(i)$ 和 $f_2(i)$,分别表示以 $i$ 为结尾的转移序列的数量和总和,那么很显然 ${} f_1(i)=\sum\limits_{j\rightarrow i}f_1(j) {}$,而我们要转移到 $i$ 时 $j$ 的每一个序列都会在末尾增加一个元素,因此 ${} f_2(i)=\sum\limits_{j\rightarrow i}(f_1(j)+f_2(j)) {}$。初始时 $f_1(i),f_2(i) \leftarrow 1$。 现在我们已经算出了所有的 $f_1$ 和 $f_2$,该如何统计答案呢?实际上,我们可以在转移的途中顺便统计答案。对于 $j\rightarrow i$ 的转移,如果 $F(l,r)$ 中的 $r$ 正好 $\in [j,i)$,那么这个状态实际上是转移不了的,这时候就可以对 $j$ 统计答案了,$ans \leftarrow (i-j)*f_2(j)$。时间复杂度 $O(n \log_2 n)$。 对于最后无法继续转移但是没有被 $r$ 限制的状态,我们可以添加一个哨兵 $a_{n+1}=+\infty$,这样遍历到 $i=n+1$ 时就会全部转移。 ```cpp #include <iostream> #include <queue> using namespace std; using LL = long long; const LL kMaxN = 4e5 + 5; LL a[kMaxN], f[kMaxN], dp[kMaxN], t, n, ans; priority_queue<pair<LL, LL>, vector<pair<LL, LL>>, greater<pair<LL, LL>>> q; int main() { cin.tie(0)->sync_with_stdio(0); for (cin >> t; t; t--) { cin >> n; for (LL i = 1; i <= n; i++) { cin >> a[i]; } a[n + 1] = 1e18; f[1] = dp[1] = 1; for (; q.size(); q.pop()) { } q.emplace(a[1], 1); ans = 0; for (LL i = 2; i <= n + 1; i++) { f[i] = dp[i] = 1; for (; q.size() && q.top().first < a[i]; q.pop()) { f[i] += f[q.top().second]; dp[i] += dp[q.top().second] + f[q.top().second]; ans += (i - q.top().second) * dp[q.top().second]; } q.emplace(a[i], i); } cout << ans << '\n'; } return 0; } ``` ## 碰瓷 #序列dp 本题我的方法错误。 我最开始的方法是,枚举 $a$ 的每一个可能的起始点 $i \in [1,n]$,然后设计 dp 状态 $f_{j,k}$ 表示为 $a_{i\sim j}$ 可以与 $b_{1\sim k}$ 进行匹配,但是这个算法不仅转移困难,而且时间复杂度高达 $O(n^2m)$。 我成功地获得了 $0$ 分的好成绩的原因是我的转移过程写错了。具体而言,对于在 $b$ 中相同位置插入多个新元素的转移,我没有一个好的转移方式能够满足所有的可能。比如 $1$ 后面添加 $3$ 再添加 $1$ 这种。 ## 旋律 #unknow 此题我采用的是暴力。 对于向前插入和向后插入的操作,我手动维护一个双端队列,分别用 $l$ 和 $r$ 表示队列的头和尾。为了防止一直往前面插入,我初始化 $l=r=10^6$,然后将数组大小初始化为 $2 \times 10^6$。 接下来考虑统计答案。观察样例,可以发现,如果我们选择前 $m$ 个数作为旋律,那么 $i \in (m,n]$ 的数都必须满足 $a_i=a_{i-m}$。因此我们使用 `vector` 的数组维护每一个 $i$,满足 $a_i=a_j(j<i)$,的 $i-j$ 的数量(即当前位置满足条件的所有的 $m$)。 然后进行倒叙枚举 $i$,使用一个表 $f$ 记录 $m$ 被"命中"的次数,将 $i$ 的我们之前记录过的所有满足条件的 $m$ 的值,对应的 $f_m$ 加上 $1$。如果有 $f_m$ 的值正好等于我们已经遍历过的元素数量($r-i+1$),那么这个答案就是符合要求的。 我这个方法最坏时间复杂度高达 $O(n^3)$,因为虽然单次修改是 $O(n)$ 的,但是所有 `vector` 里面的元素数量最坏是 $O(n^2)$ 量级的(即所有字母都一样的情况),因此时间复杂度为 $O(n^3)$,只能通过 $40$ 分。 ```cpp #include <iostream> #include <vector> using namespace std; const int kMaxN = 2e6 + 5; int f[kMaxN], n, l, r, ans; char s[kMaxN], c; vector<int> v[kMaxN]; void insertLeft(char c) { s[--l] = c; } void insertRight(char c) { s[++r] = c; } int main() { cin.tie(0)->sync_with_stdio(0); cin >> n; l = 1e6, r = l - 1; for (int i = 1, o; i <= n; i++) { cin >> o >> c; if (o == 1) { insertLeft(c); for (int i = l + 1; i <= r; i++) { if (s[i] == c) { v[i].push_back(i - l); } } } else { insertRight(c); for (int i = l; i < r; i++) { if (s[i] == c) { v[r].push_back(r - i); } } } fill(f + 1, f + r - l + 1, 0); ans = 1; for (int i = r; i > l; i--) { for (int j : v[i]) { f[j]++; } ans += f[i - l] == r - i + 1; } cout << ans << '\n'; } return 0; } ``` ## 非负 #unknow 这道题目我只骗了 $2$ 分,即 $a_i=(-1)^{i+1}$ 的情况。 对于这种情况,首先如果 $a_l$ 或 $a_r$ 等于 $-1$ 那 $[l,r]$ 肯定是不满足条件的,因为长度为 $1$ 的前缀和后缀不大于等于 $0$,此时我们直接 $l \leftarrow l+1$ 或 $r \leftarrow r-1$,这样因为 $a_i$ 是 $1,-1$ 交替出现,所以在我们操作之后 $a_l$ 和 $a_r$ 都是 $1$。 接下来,既然 $a_i$ 是 $1$ 和 $-1$ 交替出现了,那么前缀和后缀的值是 $1,0$ 交替出现的,满足条件,直接输出 $\max(0,r-l+1)$,成功获得 $2$ 分。 ```cpp #include <iostream> #include <set> using namespace std; const int kMaxN = 5e5 + 5; int a[kMaxN], p[kMaxN], dp[kMaxN], n, q; int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> q; for (int i = 1; i <= n; i++) { cin >> a[i]; } for (int o, p, l, r; q; q--) { cin >> o; if (o == 1) { cin >> p; a[p] = -a[p]; } else { cin >> l >> r; if (a[l] == -1) { l++; } if (a[r] == -1) { r--; } cout << max(0, r - l + 1) << '\n'; } } return 0; } ``` ## 总结 本次得分 $100+0+40+2=142$。 问题就是,第二题暴力写挂了,直接蒸发 $80$ 分;第四题最开始看错题了,写了一个小时之后才发现,后面就没有时间写正经暴力,连 $15$ 分的大爆搜也没写。 以后再次遇到像第二题这样的题目时,我应该更仔细地进行转移,避免犯同样的错误。同时稳住心态,多尝试不同的转移方式,因为我当时的状态设计和转移思路已经和正确的暴力 dp 一样了。下次再次遇到这种题目时,我应该更加冷静地思考,避免犯同样的错误,争取把该拿的分拿到手,之后再去写后面的题目。