题意:有一个 2 × n 2\times n 2 × n 的矩阵,每个格子有可能是干净的也有可能是脏的。一个机器人从 ( 1 , 1 ) (1,1) ( 1 , 1 ) 出发,每次移动到离他的曼哈顿距离最近的脏格子并清理。如果出现曼哈顿距离相同的两个脏格子,则机器人会发生故障。在机器人出发前,你可以手动清理若干个格子,问最多保留多少个脏格子能使机器人不出故障地清理完。
注意到,机器人一定会从左往右清理,所以同时出现的两个脏格子只可能出现在当前位置的右侧。定义 f [ i ] [ j ∈ { 0 , 1 } ] [ k ∈ { 0 , 1 } ] f[i][j\in\{0,1\}][k\in\{0,1\}] f [ i ] [ j ∈ { 0 , 1 } ] [ k ∈ { 0 , 1 } ] 表示当前在第 i i i 列的第 j j j 行,上一列有/没有拐弯时最多能留多少脏格子,则如果上一列拐了弯,这一列就不能再拐一次(否则会冲突),所以要手动清理对面的位置;如果上一列没有拐弯,这一列可以拐(让机器人清理对面的位置)也可以不拐(手动清理),转移即可。
By Unique_Hanpi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int MAXN = 2e5 +5 ;const int Mod = 998244353 ;int n, f[MAXN][2 ][2 ];char s[2 ][MAXN];inline void chkMax (int &a, int b) { if (a < b) a = b; }void solve () { scanf ("%d" , &n); scanf (" %s %s" , s[0 ] + 1 , s[1 ] + 1 ); memset (f, -1 , sizeof (f)); f[1 ][0 ][0 ] = 0 ; for (int i = 1 ; i <= n; i++) for (int j = 0 ; j < 2 ; j++) for (int k = 0 ; k < 2 ; k++) if (~f[i][j][k]) { if (s[j ^ 1 ][i] == '1' && !k) chkMax (f[i + 1 ][j ^ 1 ][1 ], f[i][j][k] + 1 + (s[j ^ 1 ][i + 1 ] == '1' )); chkMax (f[i + 1 ][j][0 ], f[i][j][k] + (s[j][i + 1 ] == '1' )); } int ans = 0 ; for (int i = 0 ; i < 2 ; i++) for (int j = 0 ; j < 2 ; j++) chkMax (ans, f[n + 1 ][i][j]); printf ("%d\n" , ans); } int main () { solve (); return 0 ; }
题意:Alice 和 Bob 轮流从序列里取数,Alice 先取,取完为止。如果最后 Alice 取到的和为偶数,Alice 赢,否则 Bob 赢。问最优决策下谁获胜。
显然可以 DP。令 f [ i ] [ j ] [ k ∈ { 0 , 1 } ] f[i][j][k\in\{0,1\}] f [ i ] [ j ] [ k ∈ { 0 , 1 } ] 表示如果还剩 i i i 个奇数、j j j 个偶数,先手能否取到 k k k 。则转移可以为:若删掉一个奇数时,剩下的 DP 值必须与先手期望的相同,或删掉一个偶数时,剩下的 DP 值必须与先手期望的相同,则此 DP 值为 1,否则为 0。
By maroonrk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const int nmax=105 ;bool ok[nmax][nmax][2 ];void initok () { ok[0 ][0 ][0 ]=true ; rep (x,nmax)rep (y,nmax){ rep (tar,2 ){ int bad=(y%2 )^tar^1 ; if (x>0 &&!ok[x-1 ][y][bad]){ ok[x][y][tar]=true ; } if (y>0 &&!ok[x][y-1 ][bad]){ ok[x][y][tar]=true ; } } } } void slv () { int n;cin>>n; vi a=readvi (n); int x=0 ,y=0 ; for (auto v:a)if (v%2 ==0 )x++; else y++; if (ok[x][y][0 ])cout<<"Alice\n" ; else cout<<"Bob\n" ; } signed main () { cin.tie (0 ); ios::sync_with_stdio (0 ); cout<<fixed<<setprecision (20 ); initok (); int t;cin>>t;rep (_,t) slv (); }
题意:有两个 01 序列 a a a 和 b b b ,每次操作可以选 a a a 的两个元素各自取反,如果相邻则代价为 x x x ,否则代价为 y y y 。求将 a a a 变成 b b b 的最小代价。
当时在 CF 评论区写的英文题解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <bits/stdc++.h> using namespace std;#define int long long int f[5010 ];vector<int > dif; signed main () { int t; cin>>t; while (t--) { dif.clear (); int n,x,y; cin>>n>>x>>y; string a,b; cin>>a>>b; for (int i=0 ;i<n;i++) if (a[i]!=b[i]) dif.push_back (i); if (signed (dif.size ())%2 !=0 ) { cout<<-1 <<endl; continue ; } if (x>=y) { if (dif.size ()==2 ) { if (dif[0 ]+1 ==dif[1 ]) { if (n!=2 ) cout<<min (x,2 *y)<<endl; else cout<<x<<endl; } else cout<<y<<endl; } else cout<<y*signed (dif.size ())/2 <<endl; } else { if (dif.size ()==0 ) { cout<<0 <<endl; continue ; } f[0 ]=f[1 ]=0 ; for (int i=2 ;i<=dif.size ();i++) { f[i]=f[i-2 ]+min (x*(dif[i-1 ]-dif[i-2 ]),y); if (i%2 ==0 ) f[i]=min (f[i],f[i-1 ]+y); else f[i]=min (f[i],f[i-1 ]); } cout<<f[dif.size ()]<<endl; } } return 0 ; }
题意:Alice 和 Bob 轮流从字符串的首或尾取字符并插入到自己字符串的开头,字典序较小者胜。如果相同则平局。问双方最优策略下的结果。
显然可以区间 DP。令 f [ i ] [ j ] f[i][j] f [ i ] [ j ] 表示还剩 [ i , j ] [i,j] [ i , j ] 时的结果,则 f [ i ] [ j ] f[i][j] f [ i ] [ j ] 可以从 f [ i + 2 ] [ j ] , f [ i + 1 ] [ j − 1 ] , f [ i ] [ j − 2 ] f[i+2][j],f[i+1][j-1],f[i][j-2] f [ i + 2 ] [ j ] , f [ i + 1 ] [ j − 1 ] , f [ i ] [ j − 2 ] 转移而来。
By jiangly
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <bits/stdc++.h> using i64 = long long ;int cmp (char a, char b) { if (a < b) { return -1 ; } else if (a == b) { return 0 ; } else { return 1 ; } } void solve () { std::string s; std::cin >> s; int n = s.length (); std::vector dp (n + 1 , std::vector<int >(n + 1 )) ; for (int r = 0 ; r <= n; r++) { for (int l = r; l >= 0 ; l--) { if (l == r) { dp[l][r] = 0 ; } else if (r - l >= 2 ) { int v1 = dp[l + 1 ][r - 1 ] != 0 ? dp[l + 1 ][r - 1 ] : cmp (s[l], s[r - 1 ]); int v2 = dp[l + 2 ][r] != 0 ? dp[l + 2 ][r] : cmp (s[l], s[l + 1 ]); int v3 = dp[l + 1 ][r - 1 ] != 0 ? dp[l + 1 ][r - 1 ] : cmp (s[r - 1 ], s[l]); int v4 = dp[l][r - 2 ] != 0 ? dp[l][r - 2 ] : cmp (s[r - 1 ], s[r - 2 ]); dp[l][r] = std::min (std::max (v1, v2), std::max (v3, v4)); } } } int ans; if (n % 2 == 0 ) { ans = dp[0 ][n]; } else { ans = std::min (dp[0 ][n - 1 ], dp[1 ][n]); if (ans == 0 ) { ans = 1 ; } } std::cout << (ans == -1 ? "Alice" : ans == 0 ? "Draw" : "Bob" ) << "\n" ; } int main () { std::ios::sync_with_stdio (false ); std::cin.tie (nullptr ); int t; std::cin >> t; while (t--) { solve (); } return 0 ; }
彩蛋:自习观察 DP 转移可以发现,Bob 无论如何也赢不了(数学归纳法)。
题意:有一个长度为 n n n 的序列,每次可以选择一个连续子段将其异或上任意值。设选择的子段长度为 l e n len l e n ,则操作的代价为 ⌈ l e n 2 ⌉ \left\lceil\frac{len}{2}\right\rceil ⌈ 2 l e n ⌉ 。求将序列全部变成 0 0 0 的最小代价。
注意到,由于操作的代价与长度近似线性相关,所以一次较长操作可以拆分成若干个小操作使代价不变。思考可以发现,选择的长度要么为 1 1 1 要么为 2 2 2 ,代价均为 1 1 1 。可以发现,总操作代价的上限就为 n n n ,每次暴力将一个数改为 0 0 0 ,考虑如何优化此策略。优化一定是将一段长度为 k k k 的区间的 k k k 次 1 1 1 操作改为 k − 1 k-1 k − 1 次二操作使得效果不变。因为如果 k − 1 k-1 k − 1 次二操作中不能将 k k k 个数清空而需要多一个一操作来“扫尾”,代价就又变回 k k k 了。什么时候可以用 k − 1 k-1 k − 1 次二操作清空 k k k 的子段呢?考虑第一次操作一定是对前两个元素同时异或第一个元素值、第二次操作是对第二个和第三个元素同时异或第二个元素剩下的元素值,以此类推。我们将它形式化地描述出来:
a 1 , a 2 ⊕ a 1 ⇒ a 1 = 0 , a 2 = a 1 ⊕ a 2 a 2 , a 3 ⊕ ( a 1 ⊕ a 2 ) ⇒ a 2 = 0 , a 3 = a 1 ⊕ a 2 ⊕ a 3 ⋯ a k − 1 , a k ⊕ ( a 1 ⊕ a 2 ⊕ ⋯ ⊕ a k − 1 ) ⇒ a k − 1 = 0 , a k = a 1 ⊕ a 2 ⊕ ⋯ ⊕ a k − 1 ⊕ a k \begin{aligned}
a_1,a_2\oplus a_1 &\Rightarrow a_1=0,a_2=a_1\oplus a_2\\
a_2,a_3\oplus(a_1\oplus a_2)&\Rightarrow a_2=0,a_3=a_1\oplus a_2\oplus a_3\\
&\cdots\\
a_{k-1},a_k\oplus(a_1\oplus a_2\oplus\cdots\oplus a_{k-1})&\Rightarrow a_{k-1}=0,a_k=a_1\oplus a_2\oplus\cdots\oplus a_{k-1}\oplus a_k
\end{aligned}
a 1 , a 2 ⊕ a 1 a 2 , a 3 ⊕ ( a 1 ⊕ a 2 ) a k − 1 , a k ⊕ ( a 1 ⊕ a 2 ⊕ ⋯ ⊕ a k − 1 ) ⇒ a 1 = 0 , a 2 = a 1 ⊕ a 2 ⇒ a 2 = 0 , a 3 = a 1 ⊕ a 2 ⊕ a 3 ⋯ ⇒ a k − 1 = 0 , a k = a 1 ⊕ a 2 ⊕ ⋯ ⊕ a k − 1 ⊕ a k
此时 k − 1 k-1 k − 1 次操作做完了,可以发现 a 1 ⋯ a k − 1 a_1\cdots a_{k-1} a 1 ⋯ a k − 1 都能保证变成 0 0 0 了,但我们要求这 k k k 个全部变成 0 0 0 ,这就要求 a 1 ⊕ a 2 ⊕ ⋯ ⊕ a k − 1 ⊕ a k = 0 a_1\oplus a_2\oplus\cdots\oplus a_{k-1}\oplus a_k=0 a 1 ⊕ a 2 ⊕ ⋯ ⊕ a k − 1 ⊕ a k = 0 ,即这个子段的异或和为 0 0 0 。于是我们可以使用前缀异或和加一个桶来维护这个信息,DP 转移就很显然了:f [ i ] = m i n ( f [ l s t ] + ( i − l s t − 1 ) , f [ i − 1 ] + 1 ) f[i]=min(f[lst]+(i-lst-1),f[i-1]+1) f [ i ] = m i n ( f [ l s t ] + ( i − l s t − 1 ) , f [ i − 1 ] + 1 ) ,其中 l s t lst l s t 表示上一个与当前位置的前缀异或和相同的位置。
注意到这个 DP 方程不需要特判本身已经为 0 0 0 的情况,因为此时的前缀异或和与上一个位置相同,l s t = i − 1 , f [ i ] = f [ i − 1 ] + 0 lst=i-1,f[i]=f[i-1]+0 l s t = i − 1 , f [ i ] = f [ i − 1 ] + 0 ,不会出现错误。
By jiangly
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <bits/stdc++.h> using i64 = long long ;void solve () { int n; std::cin >> n; std::vector<int > a (n) ; for (int i = 0 ; i < n; i++) { std::cin >> a[i]; } std::vector<int > s (n + 1 ) ; for (int i = 0 ; i < n; i++) { s[i + 1 ] = s[i] ^ a[i]; } std::vector<int > dp (n + 1 ) ; std::map<int , int > lst; lst[s[0 ]] = 0 ; for (int i = 1 ; i <= n; i++) { dp[i] = dp[i - 1 ]; if (lst.count (s[i])) { dp[i] = std::max (dp[i], 1 + dp[lst[s[i]]]); } lst[s[i]] = i; } std::cout << n - dp[n] << "\n" ; } int main () { std::ios::sync_with_stdio (false ); std::cin.tie (nullptr ); int t; std::cin >> t; while (t--) { solve (); } return 0 ; }
这份代码的写法是考虑优化了几个子段,再用 n n n 减去优化的段数。本质上是相同的。
题意:有 n n n 个储水槽,第 i i i 个的容量为 v i v_i v i 升,当一个储水槽满后会流向下一个储水槽,第 n n n 个储水槽后面是河流。每个储水槽上面都有一个水阀,如果打开水阀则会有 1 1 1 升每秒的水流入。每次询问给一个时间 t t t ,回答至少要开多少水阀才能在 t t t 时间内填满全部储水槽。
容易想到反向考虑:若打开 i i i 个水阀,最短在多少时间内填满全部储水槽。设这个答案为 f [ i ] f[i] f [ i ] 。注意到,如果储水槽个数固定,打开的一定是最靠前的若干个储水槽,这给了我们 DP 的机会。为了计算 f [ i ] f[i] f [ i ] ,我们定义 g [ i ] g[i] g [ i ] 表示打开前 i i i 个水阀,最短在多少时间内填满前 i i i 个储水槽,以及 h [ i ] h[i] h [ i ] 表示在 g [ i ] g[i] g [ i ] 时间过后会多出来多少水流到后面。g g g 和 h h h 的转移容易想到,则 f f f 也可以很容易地由 g , h g,h g , h 求得。显然 f f f 有单调性,所以对于每个询问二分查找即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <bits/stdc++.h> using namespace std;#define int long long int v[200010 ],s[200010 ];int f[200010 ],g[200010 ],h[200010 ];signed main () { int n,q; cin>>n; for (int i=1 ;i<=n;i++) cin>>v[i]; for (int i=1 ;i<=n;i++) s[i]=s[i-1 ]+v[i]; for (int i=1 ;i<=n;i++) { int tmp=v[i]-h[i-1 ]-g[i-1 ]; if (tmp<=0 ) g[i]=g[i-1 ],h[i]=-tmp; else { g[i]=g[i-1 ]+(tmp+i-1 )/i; h[i]=i*g[i]-s[i]; } } for (int i=1 ;i<=n;i++) f[i]=g[i]+max (0ll ,(s[n]-s[i]-h[i]+i-1 )/i); reverse (f+1 ,f+n+1 ); cin>>q; while (q--) { int t; cin>>t; int tmp=upper_bound (f+1 ,f+n+1 ,t)-f-1 ; if (tmp==0 ) cout<<-1 <<endl; else cout<<n+1 -tmp<<endl; } return 0 ; }
题意:有一个变量,初始为 0 0 0 ,第 i i i 次操作可以将它增加 k + i − 1 k+i-1 k + i − 1 的若干倍(不能为 0 0 0 倍)。对于每个 x ∈ [ 1 , n ] x\in [1,n] x ∈ [ 1 , n ] ,输出到达 x x x 的方案数。
可以设 f [ i ] [ j ] f[i][j] f [ i ] [ j ] 表示最后一次操作为 j j j 的倍数(即第 j − k + 1 j-k+1 j − k + 1 次操作)情况下达到 i i i 的方案数,则
f [ i ] [ j ] = ∑ p j ≤ i f [ i − p j ] [ j − 1 ] f[i][j]=\sum_{pj\le i} f[i-pj][j-1]
f [ i ] [ j ] = p j ≤ i ∑ f [ i − p j ] [ j − 1 ]
我们可以发现,f [ i − j ] [ j ] f[i-j][j] f [ i − j ] [ j ] 与 f [ i ] [ j ] f[i][j] f [ i ] [ j ] 的形式十分相近,区别只在于 f [ i ] [ j ] f[i][j] f [ i ] [ j ] 比 f [ i − j ] [ j ] f[i-j][j] f [ i − j ] [ j ] 多了一个 f [ i − j ] [ j − 1 ] f[i-j][j-1] f [ i − j ] [ j − 1 ] 的项,所以可以使用类似完全背包的同层内部转移的方法来优化。同时,第二维可以舍去(或用滚动数组优化)。
接下来考虑时间复杂度。由于增加不能为 0 0 0 倍,在 j j j 最大的情况下即为每次增加 k + i − 1 k+i-1 k + i − 1 的 1 1 1 倍。此时 j j j 次操作后变量最小为 j ( j + 1 ) / 2 j(j+1)/2 j ( j + 1 ) / 2 ,而我们只需要 DP 到 n n n ,所以 i i i 最大为 n \sqrt{n} n 级别,可以通过此题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <bits/stdc++.h> using namespace std;const int mod=998244353 ;int f[200010 ][2 ];int ans[200010 ];int main () { int n,k; cin>>n>>k; f[0 ][k%2 ]=1 ; for (int j=k;;j++) { if (j*(j+1 )/2 -k*(k-1 )/2 >n) break ; int x=j%2 ; for (int i=1 ;i<=n;i++) { if (j*(j+1 )/2 -k*(k-1 )/2 >i) { f[i][x]=0 ; continue ; } f[i][x]=(f[i-j][x]+f[i-j][x^1 ])%mod; ans[i]=(ans[i]+f[i][x])%mod; } } for (int i=1 ;i<=n;i++) cout<<ans[i]<<" " ; cout<<endl; return 0 ; }
题意:有一个可重集,初始状态给定,每次操作可以选择一个元素分解成两个数的乘积,求任意次操作后最小极差。
考虑从大到小计算以 i i i 为最小值的最小的最大值,此时我们可以将每个 ≥ i 2 \ge i^2 ≥ i 2 的 i i i 的倍数都除以 i i i 。考虑设 f [ j ] f[j] f [ j ] 表示当前状态下 j j j 能转换成的最小值,同时开个桶维护已有元素,则每次枚举最小值 i i i 时更新所有满足条件的 f [ j ] f[j] f [ j ] 并更新桶,最后缩小上界即可。
By jiangly
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <bits/stdc++.h> using i64 = long long ;void solve () { int n, m; std::cin >> n >> m; std::vector<bool > have (m + 1 ) ; int min = m; for (int i = 0 ; i < n; i++) { int a; std::cin >> a; min = std::min (min, a); have[a] = true ; } std::vector<int > cnt (m + 1 ) ; std::vector<int > dp (m + 1 , m) ; for (int i = 1 ; i <= m; i++) { if (have[i]) { cnt[m]++; } } int ans = m; int max = m; for (int i = m; i > 0 ; i--) { if (have[i]) { cnt[dp[i]]--; } dp[i] = i; if (have[i]) { cnt[dp[i]]++; } for (int j = 1 ; i * j <= m; j++) { if (have[i * j]) { cnt[dp[i * j]]--; } dp[i * j] = std::min (dp[i * j], std::max (dp[j], i)); if (have[i * j]) { cnt[dp[i * j]]++; } } while (!cnt[max]) { max--; } if (i <= min) { ans = std::min (ans, max - i); } } std::cout << ans << "\n" ; } int main () { std::ios::sync_with_stdio (false ); std::cin.tie (nullptr ); int t; std::cin >> t; while (t--) { solve (); } return 0 ; }
题意:有 n n n 堆石子,每次操作可以将一堆石子中的一个移到相邻一堆,问将石子序列变成不上升序列的最小操作次数。
令 f [ i ] [ j ] [ k ] f[i][j][k] f [ i ] [ j ] [ k ] 表示前 i i i 个数,和为 j j j ,最后一个位置改成 k k k 的最小代价。考虑如何转移:f [ i − 1 ] [ j ] [ x ] f[i-1][j][x] f [ i − 1 ] [ j ] [ x ] 可以转移到 f [ i + 1 ] [ j + k ] [ k ] f[i+1][j+k][k] f [ i + 1 ] [ j + k ] [ k ] ,此时 x ≤ k x\le k x ≤ k