一些思维题(三)
Problem
1492 C. Maximum width
给两个字符串 s 和 t ,s的长度为n, t的长度为m, 且 t 为 s 的子串
也就是可以取p1,p2,p3....pm,使得t[pi] == s[i] (p1 < p2 < ...< pm)
求max(pi+1 - pi) 最大为多少
2 <= m <= n <= 2e5
1499 D. The Number of Pairs
T组输入,每组输入三个数c,d,x
求满足c * lcm(a,b) - d * gcd(a,b) = x的a,b有多少对
例:c = 1,d = 1, x = 3 , a,b可取(1,4),(4,1),(3,6),(6,3)
1 <= t <= 1e4 , 1<= c,d,x <= 1e7
1499 E. Chaotic Merge
有两个字符串x,y
字符串z一开始是空串,每次操作,可以从x串或y串左端剪切一个字母,粘贴到z串的右端,一直重复操作直到x串和y串都变为空串为止
我们称一个字符串是chaotic的,当且仅当这个字符串所有相邻的两个字符不同
输入两个串 s 和 t
f (L1,R1,L2,R2) 表示x串等于s串的[L1,R1]部分,y串等于 t串的[L2,R2]部分时,有多少种不同的操作序列,使得 z串是chaotic的
求Σf(L1,R1,L2,R2) (1<=L1<=R1<=len(s) ,1<=L2<=R2<=len(t))
答案对998244353取膜
1 <= len(s),len(t) <= 1000
Solution
1492 C. Maximum width
我们想要让某两个相邻的pi相差最大,来更新ans
那么就是考虑对于某个数x, px+1 - px 最大是多少
那么就要使px越小越好,px+1越大越好
求pi的最小值,就是把s串从左往右扫一遍,一旦匹配的上就去匹配
同理求pi的最大值,就是把s串从右往左扫一遍
code:
#include<iostream> #include<algorithm> #include<cstring> using namespace std; const int MAXN = 2e5+7; int pos1[MAXN],pos2[MAXN]; int main() { int n,m; string s,t; cin>>n>>m; cin>>s>>t; s = " " + s; t = " " + t; int pos = 1; for(int i = 1;i <= n;i++){ if(s[i] == t[pos]){ pos1[pos] = i; pos++; if(pos > m) break; } } pos = m; for(int i = n;i;i--){ if(s[i] == t[pos]){ pos2[pos] = i; pos--; if(pos<=0) break; } } int ans = pos2[2] - pos1[1]; for(int i = 3;i <= m;i++){ ans = max(ans,pos2[i] - pos1[i-1]); } cout<<ans<<endl; return 0; }
1499 D. The Number of Pairs
首先得考虑gcd和lcm能取什么
可以发现,lcm % gcd == 0,gcd % gcd == 0, (c * lcm - d * gcd) % gcd = 0 = x % gcd
那么gcd只能是x的因数了
于是我们可以枚举gcd,再通过c * lcm - d * gcd = x这个方程算出lcm,然后检查lcm是否为gcd的倍数
当gcd(a,b)和lcm(a,b)都确定下来后,怎么算a,b有多少对?
这个可以从素数分解考虑,可以自己分析
可以考虑只有一个质因数的情况,然后多质因数的情况就是乘法原理了
但是这样还会TLE,怎么办?
可以用记忆化搜索的方法,也可以用离线算法
code:
#include<iostream> #include<algorithm> #include<cstring> using namespace std; const int MAXN = 2e7 + 7; int f[MAXN], zx[MAXN], zs[MAXN], tot = 0; void pre(int n) { for (int i = 2; i <= n; i++) { if (!zx[i]) zs[++tot] = zx[i] = i; for (int j = 1; j <= tot; j++) { if (n / zs[j] < i) break; zx[i * zs[j]] = zs[j]; if (zx[i] == zs[j]) break; } } } int ins(int x) {//lcm / gcd == x的时候,(a,b)的对数有多少对 if (f[x]) return f[x];//记忆化 int t = x; int cnt = 0; while (t != 1) { int xx = zx[t]; cnt++; while (t % xx == 0) t /= xx; } return f[x] = 1 << cnt; } int main() { int T; pre(MAXN); long long a, b, c, d, x; cin >> T; while (T--) { cin >> c >> d >> x; long long gd, lc; int ans = 0; for (long long i = 1; i * i <= x; i++) { if (x % i == 0) { gd = i;//gcd = i lc = x + d * gd;//lcm if (lc % c == 0) { lc /= c; if (lc % gd == 0) { ans += ins(lc / gd); } } if (x / gd != gd) {//gcd = x / i gd = x / gd; lc = x + d * gd; if (lc % c == 0) { lc /= c; if (lc % gd == 0) { ans += ins(lc / gd); } } } } } cout << ans << endl; } return 0; }
1499 E. Chaotic Merge
首先来思考这两个串都取最大区间的情况,即完整的s串和完整的t串,有多少种操作序列
可以想到这是一个字符串上的dp题
最经典的字符串dp题就是求两个串的最长公共子串了,这个经典的问题可以用dp[ i ][ j ]表示 s取[1, i ],t 取[1, j ] 时的最长公共子串长度
那么这题也类似,先考虑用dp[ i ][ j ]表示s串取[1, i ], t串取[1, j ]时的操作序列数
思考转移过程的时候发现这样还是不够的,我们要关注z串最后一个字母是什么,这便要看最后一个操作是取x串的还是取y串的了
于是我们用dp[ i ][ j ][ 0 ]表示s串取[1, i ],t串取[1, j ],且最后一个操作是取x串的操作序列个数, dp[ i ][ j ][ 1 ]则表示的是最后一个操作是取y串的
在草稿纸上可以推出4个转移方程
转移方程的代码如下:
dp[1][0][0] = dp[0][1][1] = 1; for(int i = 1;i <= n;i++){ for(int j = 1;j <= m;j++){ if(a[i] != a[i - 1]){ dp[i][j][0] += dp[i - 1][j][0]; } if(a[i] != b[j]){ dp[i][j][0] += dp[i - 1][j][1]; } if(b[j] != b[j - 1]){ dp[i][j][1] += dp[i][j - 1][1]; } if(b[j] != a[i]){ dp[i][j][1] += dp[i][j - 1][0]; } } }
这个子问题就解决了,子问题的难度大概在1500分左右吧
我们发现解决这个子问题的同时,我们不仅求出了s串和t串取最大区间时的答案,还求出了s串取[1, i ]时,t串取[1, j]时的答案,也就是求出了len(s)*len(t)个答案
如果我们还要再求所有s串取[1, i ]时,t 串取[2, j ]时的答案,那就要再跑一遍dp转移方程,但是这次跑的时候,要初始化一遍,起点也不一样了,但是哪些点转移到哪些点还是不变的!
所以我们每次重新跑一遍dp的时候,就是起点变了而已
举个简单的例子,假设有dp[ i ] = dp[ i - 1] + dp[i - 2] + i,有一次从dp[1] = dp[2] = 0开始跑,另一次是从dp[4] = dp[5] = 0开始跑,这就是只有起点变了
分析仅仅是起点变了,贡献有哪些变化
我们可以把dp[ i ][ j ][ k ]看成是一个点,把有转移关系的点之间连一条有向边
如图,之前讲的子问题,就是从dp[1][0][0] = 1和dp[0][1][1] = 1这两个起点开始
如果把下面的起点换个位置
那么就是这种情况了
两个起点分别的贡献是:
和
显然这两个起点是独立的
现在我们要做的是,如何快速求出,当一个点为起点时,且这个起点上的值为1,能产生多少贡献
这显然又是一个dp
我是用siz[i][j][k]表示这个点为起点且点上的值为1时,产生的贡献为多少
关于siz[i][j][k]的递推代码 (注意初始化和取膜) :
for(int i = 0;i<=n;i++) for(int j = 0;j<=m;j++) siz[i][j][0] = siz[i][j][1] = 1; for(int i = n;i>=0 ;i--){ for(int j = m;j>=0 ;j--){ if(i && a[i] != a[i-1]) { //dp[i][j][0] += dp[i-1][j][0]; //dp[i][j][0]表式a串取1到i,b串取1到j,要使merge串的最后一位是a串的最后一位的构造方案数 siz[i-1][j][0] += siz[i][j][0]; siz[i-1][j][0] %= MOD; } if(i && a[i] != b[j]){ //dp[i][j][0] += dp[i-1][j][1]; siz[i-1][j][1] += siz[i][j][0]; siz[i-1][j][1] %= MOD; } if(j && b[j] != b[j-1]){ //dp[i][j][1] += dp[i][j-1][1]; siz[i][j-1][1] += siz[i][j][1]; siz[i][j-1][1] %= MOD; } if(j && b[j] != a[i]){ //dp[i][j][1] += dp[i][j-1][0]; siz[i][j-1][0] += siz[i][j][1]; siz[i][j-1][0] %= MOD; } } }
还要注意x串和y串是不能为空的,所以还要减去x串或y串为空的情况
比如在最开始的子问题里,要减去所有dp[i][0][0],减去所有dp[0][j][1]
这个也可以用dp算出某个点为起点时,能有多少种方案
我是用sza[i]和szb[i]表示
for(int i = 1;i<=n;i++) sza[i] = 1; for(int i = n;i;i--){ if(a[i] != a[i-1]) sza[i-1] += sza[i]; } for(int i = 1;i<=m;i++) szb[i] = 1; for(int i = m;i;i--){ if(b[i] != b[i-1]) szb[i-1] += szb[i]; }
总的代码:
#include<iostream> #include<algorithm> #include<cstring> #include<cstdio> using namespace std; const int MAXN = 1e3+7; const long long MOD = 998244353; int n,m; long long dp[MAXN][MAXN][2]; long long siz[MAXN][MAXN][2]; int sza[MAXN],szb[MAXN]; int main() { string a,b; cin >> a >> b; n = a.length();m = b.length(); a = " " + a; b = " " + b; int u,v; long long ans = 0; for(int i = 0;i<=n;i++) for(int j = 0;j<=m;j++) siz[i][j][0] = siz[i][j][1] = 1; for(int i = n;i>=0 ;i--){ for(int j = m;j>=0 ;j--){ if(i && a[i] != a[i-1]) { //dp[i][j][0] += dp[i-1][j][0]; //dp[i][j][0]表式a串取1到i,b串取1到j,要使merge串的最后一位是a串的最后一位的构造方案数 siz[i-1][j][0] += siz[i][j][0]; siz[i-1][j][0] %= MOD; } if(i && a[i] != b[j]){ //dp[i][j][0] += dp[i-1][j][1]; siz[i-1][j][1] += siz[i][j][0]; siz[i-1][j][1] %= MOD; } if(j && b[j] != b[j-1]){ //dp[i][j][1] += dp[i][j-1][1]; siz[i][j-1][1] += siz[i][j][1]; siz[i][j-1][1] %= MOD; } if(j && b[j] != a[i]){ //dp[i][j][1] += dp[i][j-1][0]; siz[i][j-1][0] += siz[i][j][1]; siz[i][j-1][0] %= MOD; } } } for(int i = 1;i<=n;i++) sza[i] = 1; for(int i = n;i;i--){ if(a[i] != a[i-1]) sza[i-1] += sza[i]; } for(int i = 1;i<=m;i++) szb[i] = 1; for(int i = m;i;i--){ if(b[i] != b[i-1]) szb[i-1] += szb[i]; } for(int l = 1;l <= n;l++){//枚举左右起点 for(int r = 1;r <= m;r++){ ans += siz[l][r-1][0] + siz[l-1][r][1] - sza[l] - szb[r]; //起点是dp[l][r-1][0] = 1 和 dp[l-1][r][1] = 1 ans = (ans % MOD + MOD) % MOD; } } cout<<ans<<"\n"; return 0; }
总结一下,这题首先是一个字符串的dp问题,然后题目还要求计算取尽所有起点的情况的答案之和
由于只有起点变化,有向边没有变,于是让人思考起点或起点上的值改变时,其对贡献的变化是怎样的