Illublog我多想说再见啊

「2017 山东一轮集训 Day6」子序列

Illu·2022-07-13 19:49·108 次阅读

「2017 山东一轮集训 Day6」子序列

复盘 nealchen 神仙讲的题。

这可以来一手反复 AC 鞭尸(

Description#

区间本质不同子序列,母串长度 n ,询问 q 次,字符集大小 ||

n, q105, ||9

Analysis#

本来还有一个区间本质不同子串,但显然这俩都不是一个类型的(

显然子序列和子串都不是一个量级的,肯定又要用到压缩状态的好手:

DP。

我们可以考虑只枚举最后一位是什么,我们可以考虑在前面 i 个里面任选,结尾是 j 的数量,令为 fi,j ,同时令结尾 m+1 的地方什么都没有填。

所以对于结尾不是 j 的地方,都只能继承上一个位置的答案,反之,则任何位置都能转移下来。

而且这不会算重:

假如存在形如 “STS” 的形式,当后面的 “S” 完成了拼接的之后,前面的 “S” 可以视作拼接成了 “S__S” 的样子,并没有重,反而正好不漏。

DP 转移如下:

fi,j={fi1,j,jSik=1m+1fi1,k,j=Si

最终答案就是:k=1mfn,k

那我们就得到了 O(qnm) 的憨憨做法。

然后你就有 40 分了!

sol0
Copy
/* */ #include using namespace std; #define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout); #define Check(a) freopen(a".in", "r", stdin), freopen(a".ans", "w", stdout); typedef long long ll; typedef unsigned long long ull; typedef std::pair pii; #define fi first #define se second #define mp std::make_pair const int mod = 1e9 + 7; template inline int M(A x) {return x;} template inline int M(A x, B ... args) {return 1ll * x * M(args...) % mod;} #define mi(x) (x >= mod) && (x -= mod) #define ad(x) (x < 0) && (x += mod) const int N = 1e5 + 10; int n, q, f[N][10], su[N][10]; char s[N]; int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cin >> (s + 1) >> q; n = strlen(s + 1); for (int i = 1, l, r; i <= q; ++i) { std::cin >> l >> r; for (int k = 0; k < 9; ++k) { f[l - 1][k] = su[l - 1][k] = 0; } f[l - 1][9] = su[l - 1][9] = 1; for (int j = l; j <= r; ++j) { for (int k = 0; k < 10; ++k) { if (k == s[j] - 'a') { f[j][k] = su[j - 1][9]; } else f[j][k] = f[j - 1][k]; if (!k) su[j][k] = f[j][k]; else su[j][k] = su[j][k - 1] + f[j][k]; mi(su[j][k]); } } std::cout << su[r][9] - 1 << "\n"; } return 0; }

Sol1#

发现转移比较单一,考虑能不能矩阵预处理一下。

因为不等于是对应位直接转移,等于至整体转移,所以就是对角线全是 1 以及其中一行全是 1 :

Ai=(1  0  0  0  01  1  1  1  10  0  1  0  00  0  0  1  00  0  0  0  1)

这样的话答案就会形如一段 A 的乘积,因为矩阵乘法没有交换律,所以除了前缀积,还要维护后缀积(逆矩阵)。

那无论是预处理还是查询就都是 O(m3) 的了。总体下来就是 O((n+q)m3)

因为常熟比较小,然后你就可以过了,不过我们并不满足(

Sol1
Copy
/* */ #include using namespace std; #define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout); #define Check(a) freopen(a".in", "r", stdin), freopen(a".ans", "w", stdout); typedef long long ll; typedef unsigned long long ull; typedef std::pair pii; #define fi first #define se second #define mp std::make_pair const int mod = 1e9 + 7; template inline int M(A x) {return x;} template inline int M(A x, B ... args) {return 1ll * x * M(args...) % mod;} #define mi(x) (x >= mod) && (x -= mod) #define ad(x) (x < 0) && (x += mod) const int N = 1e5 + 10; int n, q; char s[N]; struct matrix { int a[10][10]; matrix() {memset(a, 0, sizeof(a));} inline void init() {for (int i = 0; i < 10; ++i) a[i][i] = 1;} matrix operator * (const matrix &it) const { matrix t; for (int i = 0; i < 10; ++i) { for (int k = 0; k < 10; ++k) { if (!a[i][k]) continue; for (int j = 0; j < 10; ++j) { t.a[i][j] += M(a[i][k], it.a[k][j]); mi(t.a[i][j]); } } } return t; } } pre[N], suf[N]; inline void init() { pre[0].init(); suf[0].init(); for (int i = 1, c; i <= n; ++i) { c = s[i] - 'a'; for (int j = 0; j < 10; ++j) { suf[i].a[c][j] = 1; pre[i].a[c][j] = mod - 1; } pre[i].init(); suf[i].init(); } for (int i = 2; i <= n; ++i) { pre[i] = pre[i - 1] * pre[i]; suf[i] = suf[i] * suf[i - 1]; } } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cin >> (s + 1) >> q; n = strlen(s + 1); init(); for (int i = 1, l, r; i <= q; ++i) { std::cin >> l >> r; matrix res = suf[r] * pre[l - 1]; int ans = 0; for (int j = 0; j < 10; ++j) { ans += res.a[j][9]; mi(ans); } std::cout << ans - 1 << "\n"; } return 0; }

Sol2#

nq 是逃不了了的,考虑优化矩阵。

很明显这个矩阵只不过是在单位矩阵的基础上加了一点点东西。

然后手玩一下,发现对于前缀积,每次乘完之后仅会有一行会整体变化,并且正好是当前字符所在的位置。

其实想想就能明白了(

所以预处理可以直接抓着那一行更新, O(nm2)

然后考虑到最后我们能用到的答案进在一列上,所以我们最后计算也只算那一列的, O(qm2)

然后就能快乐的得到 O((n+q)m2) 的代码了。

现在你可以几乎可以在 0.1s 之内通过任何数据(

Sol2
Copy
/* */ #include using namespace std; #define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout); #define Check(a) freopen(a".in", "r", stdin), freopen(a".ans", "w", stdout); typedef long long ll; typedef unsigned long long ull; typedef std::pair pii; #define fi first #define se second #define mp std::make_pair const int mod = 1e9 + 7; template inline int M(A x) {return x;} template inline int M(A x, B ... args) {return 1ll * x * M(args...) % mod;} #define mi(x) (x >= mod) && (x -= mod) #define ad(x) (x < 0) && (x += mod) const int N = 1e5 + 10; int n, q; char s[N]; struct matrix { int a[10][10]; matrix() {memset(a, 0, sizeof(a));} inline void init() {for (int i = 0; i < 10; ++i) a[i][i] = 1;} } pre[N], suf[N]; inline void init() { pre[0].init(); suf[0].init(); for (int i = 0, c; i < 10; ++i) { c = s[1] - 'a'; pre[1].a[c][i] = 1; suf[1].a[c][i] = mod - 1; pre[1].a[i][i] = suf[1].a[i][i] = 1; } for (int i = 2, c; i <= n; ++i) { c = s[i] - 'a'; pre[i] = pre[i - 1]; for (int j = 0; j < 10; ++j) { pre[i].a[c][j] = 0; for (int k = 0; k < 10; ++k) { pre[i].a[c][j] += pre[i - 1].a[k][j]; mi(pre[i].a[c][j]); } } suf[i] = suf[i - 1]; for (int j = 0; j < 10; ++j) { for (int k = 0; k < 10; ++k) { if (k == c) continue; suf[i].a[j][k] -= suf[i - 1].a[j][c]; ad(suf[i].a[j][k]); } } } } int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cin >> (s + 1) >> q; n = strlen(s + 1); init(); for (int i = 1, l, r; i <= q; ++i) { std::cin >> l >> r; int ans = 0; for (int j = 0; j < 10; ++j) { for (int k = 0; k < 10; ++k) { ans += M(pre[r].a[j][k], suf[l - 1].a[k][9]); mi(ans); } } std::cout << ans - 1 << "\n"; } return 0; }

Sol3#

感觉优化预处理已经变得比较麻烦了,先来想想查询。

我们每次都要把最后一列用矩阵再还原出来。但其实这个还原很大一部分都只是无聊的枚举,可以直接在预处理解决, O(qm)

我们前面已经提到了矩阵的变化仅限于其中一行,我们又想对这一行下手了:

乘了一个第一行满的:

(2  1  2  2  21  1  1  1  10  0  1  0  00  0  0  1  00  0  0  0  0)

再来一次:

(3  2  4  4  41  1  1  1  10  0  1  0  00  0  0  1  00  0  0  0  0)

乘了一个第三行满的:

(3  2  4  4  41  1  1  1  14  3  6  6  60  0  0  1  00  0  0  0  0)

我们发现好像整一整行的整体变化就是那一列所有的数加起来,因为那一行全部都是 1 ,其他的按照单位矩阵,就会直接复制过去。

简单维护一下列的和就行了, O(nm) 的,这个只是对于前缀积的情况。

对于后缀积呢?

甚至不用打表,从写的式子上都看得出来,对于每一行,就是:

假如修改位置 3

(a1, a2, a3, a4, a5)

(a1a3, a2a3, a3, a4a3, a5a3)

然后你就可以对于每一行维护一个 m 指之前减了多少,注意修改的位置并没有变化,所以就只需要保证 a3 不变时统一 m 就行:

(a1m, a2m, a3m, a4m, a5m)

(a1a3, a2a3, (2a3m)a3, a4a3, a5a3)

对鎏!!1

那每次递推都是 O(m) 的。

统一起来就是 O((n+q)m) 的了。

虽然实测这个只比 Sol2 快了 2.5 倍左右(

Sol3
Copy
/* */ #include using namespace std; #define File(a) freopen(a".in", "r", stdin), freopen(a".out", "w", stdout); #define Check(a) freopen(a".in", "r", stdin), freopen(a".ans", "w", stdout); typedef long long ll; typedef unsigned long long ull; typedef std::pair pii; #define fi first #define se second #define mp std::make_pair const int mod = 1e9 + 7; template inline int M(A x) {return x;} template inline int M(A x, B ... args) {return 1ll * x * M(args...) % mod;} #define mi(x) (x >= mod) && (x -= mod) #define ad(x) (x < 0) && (x += mod) const int N = 1e5 + 10; int n, q, fp[N][10], fs[N][10], pr[10][10], su[10][10]; char s[N]; inline void init() { for (int j = 0; j < 10; ++j) { pr[j][j] = su[j][j] = fp[0][j] = 1; } for (int i = 1, ch; i <= n; ++i) { ch = s[i] - 'a'; for (int j = 0; j < 10; ++j) { fp[i][j] = (fp[i - 1][j] << 1) - pr[ch][j]; mi(fp[i][j]); ad(fp[i][j]); pr[ch][j] = fp[i - 1][j]; fs[i][j] = su[ch][j]; su[ch][j] = (su[ch][j] << 1) - fs[i - 1][j]; mi(su[ch][j]); ad(su[ch][j]); } } } int ans; int main() { std::ios::sync_with_stdio(false); std::cin.tie(nullptr); std::cin >> (s + 1) >> q; n = strlen(s + 1); init(); for (int i = 1, l, r; i <= q; ++i) { std::cin >> l >> r; ans = fp[r][9] - 1; for (int j = 0; j < 9; ++j) { ans -= M(fp[r][j], fs[l - 1][j]); ad(ans); } std::cout << ans << "\n"; } return 0; }

深深明白自己的弱小。

posted @   Illusory_dimes  阅读(108)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
点击右上角即可分享
微信分享提示
目录