zzy_dp 专题总结
- [AGC034E] Complete Compress
- New Year and Original Order
- [AGC024F] Simple Subsequence Problem
- 某位歌姬的故事
- [POI2015] MYJ
- Periodni
- [AGC026D] Histogram Coloring
- [JOI Open 2016] 摩天大楼
- [USACO19DEC] Tree Depth P
- [BZOJ3864]--Hero meet devil
- LOJ 6274 - 数字
- Yet Another Minimization Problem
- [USACO19FEB] Mowing Mischief P
- Modest Substrings
- [AGC022E] Median Replace
[AGC034E] Complete Compress
\qquad 题面
\qquad 因为最终所有棋子都要走到同一点上,所以我们不妨枚举这个点,并以它作为根,然后开始考虑如何求出最小操作次数。
\qquad 首先,我们要明确一点:如果两颗棋子在同一子树内,那么我们让他们相互靠近对最终结果是毫无意义的。所以,我们每次一定会选择两颗来自不同子树的棋子,让它们共同向根移动。形式化的描述这一过程就是:每次选择两颗来自不同子树的棋子 ( x , y ) (x,y) (x,y),让 d e p x dep_x depx 减一, d e p y dep_y depy 减一( d e p i dep_i depi 是点 i i i 的深度)。把模型转化一下,问题就相当于:现在有 s o n x son_x sonx 堆石子( s o n x son_x sonx 表示 x x x 的子树数量),每次可以选择从不同的两堆中分别取出一块石子,求取完所有石子最少的操作次数,或报告无解。在这里,第 i i i 堆石子的大小是以 i i i 为根的子树中所有棋子的深度之和,记作 d i s i dis_i disi。这个 i i i 表示根的第 i i i 个儿子。
\qquad 解决这个问题是较简单的。我们设 f x f_x fx 表示以 x x x 为根的子树最多取走多少对石子。考虑分类讨论:若 d i s r o o t % 2 = 1 dis_{root}\%2=1 disroot%2=1,那么一定无解;否则我们考虑分两种情况讨论:1、 s u m − m a x ≥ m a x sum-max\geq max sum−max≥max,此时可以证明一定是可以取完的, f x = s u m 2 f_x=\frac{sum}{2} fx=2sum;2、 s u m − m a x ≤ m a x sum-max\leq max sum−max≤max,此时一定会先用最大的一堆把剩下的取完,然后还能取多少呢?结论是 min ( f v , 2 × m a x − s u m 2 ) \min(f_v,\frac{2\times max-sum}{2}) min(fv,22×max−sum), v v v 是最大的一堆对应的子树。因为剩下的石子一定要在 v v v 子树内自行解决,但是一共也就只剩了 2 × m a x − s u m 2 \frac{2\times max-sum}{2} 22×max−sum 堆,所以要取 min \min min。这样,这道题就结束了。判无解就看 f x f_x fx 与 d i s x 2 \frac{dis_x}{2} 2disx 的关系即可。
\qquad Code
New Year and Original Order
\qquad 题面
\qquad 直接做显然是不好做的,我们考虑单独考虑每一个数字带来的贡献。写成式子就是 ∑ i = 1 9 i × c n t ( i ) \sum_{i=1}^{9}i\times cnt(i) ∑i=19i×cnt(i),在这里 c n t ( i ) = ∑ d i g i t x = i 1 0 x cnt(i)=\sum_{digit_x=i}10^x cnt(i)=∑digitx=i10x。回忆学期望时的一个式子: E ( x ) = ∑ i P ( x = i ) = ∑ P ( x ≥ i ) E(x)=\sum iP(x=i)=\sum P(x\geq i) E(x)=∑iP(x=i)=∑P(x≥i),所以在这里我们也可以类比着化简式子: a n s = ∑ i = 1 9 ∑ j = i 9 c n t ( j ) ans=\sum_{i=1}^9\sum_{j=i}^9cnt(j) ans=∑i=19∑j=i9cnt(j)。写成这个形式就好写多了。我们对于每一个数字 d d d 大力数位 d p dp dp,设 d p i , j , 0 / 1 dp_{i, j, 0/1} dpi,j,0/1 表示前 i i i 位,大于等于 d d d 的数位有 j j j 个,是否压上界的方案数。转移正常枚举下一位填什么即可。不过因为 c n t ( i ) cnt(i) cnt(i) 表示 ∑ d i g i t x = i 1 0 x \sum_{digit_x=i}10^x ∑digitx=i10x,所以在最后我们还要乘一个 ∑ i = 0 j − 1 1 0 i \sum_{i=0}^{j-1}10^i ∑i=0j−110i。
\qquad Code
[AGC024F] Simple Subsequence Problem
\qquad 题面
\qquad 设 d p i , A , B dp_{i, A, B} dpi,A,B 表示长度为 i i i,已匹配的串为 A A A,可以用来匹配的串为 B B B,最多是 S S S 中 d p i , A , B dp_{i,A,B} dpi,A,B 个串的子串。转移考虑往后扩展 B B B 中的一个 0 / 1 0/1 0/1。但是因为 S S S 和 T T T 若分别存储空间承受不住,不过 S + T S+T S+T 的空间是可以接受的,所以我们可以把 S S S 和 T T T 拼接起来。但是因为可能会有前导 0 0 0,所以我们在最前面填个盖子 1 1 1。然后正常转移即可。
\qquad Code
某位歌姬的故事
\qquad 题面
\qquad n n n 很大,先离散化。离散化之后注意到每个限制都是在给一段区间赋取值上限,而可以满足限制的位置一定是这一段区间内取值上限等于 h i h_i hi 的位置。所以我们考虑按照不同的 h i h_i hi 分别 d p dp dp,最后合并方案数即可。注意到一个性质:小区间若被大区间包含(合法情况下),那么小区间可以直接删去。之后我们就可以开始 d p dp dp:设 d p i , j dp_{i,j} dpi,j 表示满足了前 i i i 个限制,且最后一个等于 h i h_i hi 的位置是 j j j 的方案数。对于相邻的两个限制,我们分相交与不相交两种情况分别处理即可。细节有点多。
\qquad 核心 C o d e Code Code:
dp[0][0] = 1;
for(int i = 1, sum, res; i <= tz; i ++) {
sum = 0, res = 1;
for(int j = L[i - 1]; j <= R[i - 1]; j ++) sum = (sum + dp[i - 1][j]) % mod;//i-1所有合法情况
gl[R[i] + 1] = 1;
for(int j = R[i]; j >= L[i]; j --) gl[j] = (1LL * gl[j + 1] * qpow(x - 1, len[P[j]])) % mod;//j~r全填hi-1的方案数
if(L[i] > R[i - 1]) {//不相交
for(int j = L[i]; j <= R[i]; j ++) {
dp[i][j] = (1LL * (1LL * (1LL * (1LL * (qpow(x, len[P[j]]) - qpow(x - 1, len[P[j]])) % mod + mod) % mod * sum) % mod * res) % mod * gl[j + 1]) % mod;
res = (1LL * res * qpow(x, len[P[j]])) % mod;
}
}
else {//相交
for(int j = L[i]; j <= R[i - 1]; j ++) dp[i][j] = (1LL * dp[i - 1][j] * gl[R[i - 1] + 1]) % mod;//特殊处理相交部分
for(int j = R[i - 1] + 1; j <= R[i]; j ++) {
dp[i][j] = (1LL * (1LL * (1LL * (1LL * (qpow(x, len[P[j]]) - qpow(x - 1, len[P[j]])) % mod + mod) % mod * sum) % mod * res) % mod * gl[j + 1]) % mod;
res = (1LL * res * qpow(x, len[P[j]])) % mod;
}
}
}
int cnt = 0;
for(int j = L[tz]; j <= R[tz]; j ++) cnt = (cnt + dp[tz][j]) % mod;
for(int i = 1; i <= tp; i ++) cnt = (1LL * cnt * (!vis[i] ? qpow(x, len[P[i]]) : 1LL)) % mod;//没有被覆盖的位置随便填
return cnt;
[POI2015] MYJ
\qquad 题面
\qquad 笛卡尔树 d p dp dp 、区间 d p dp dp 经典题。既然一个人只会去区间中价格最小的位置洗车,所以我们在区间 d p dp dp 的状态中加一维最小值即可。设 d p l , r , k dp_{l,r,k} dpl,r,k 表示区间 [ l , r ] [l,r] [l,r] 最小值为 k k k 时花钱最大是多少。转移时对于每一个 [ l , r ] [l,r] [l,r] 预处理一个 c n t l o c , v a l cnt_{loc, val} cntloc,val 表示在 l o c loc loc 处设置价格 v a l val val 有多少人在此洗车。正常区间 d p dp dp 转移即可。因为最终要输出方案,所以转移过程中顺带记录一下最优决策点即可。
\qquad 核心 C o d e Code Code:
for(int k = 1; k <= m; k ++) {
if(l <= a[k].a && a[k].b <= r) {
for(int h = a[k].a; h <= a[k].b; h ++) {
cnt[h][a[k].c] ++;
}
}
}
for(int k = l; k <= r; k ++)
for(int h = lx - 1; h; h --)
cnt[k][h] += cnt[k][h + 1];//价格少肯定包含价值多
for(int k = lx; k; k --) {
dp[l][r][k] = dp[l][r][k + 1], Min[l][r][k] = Min[l][r][k + 1], opt[l][r][k] = opt[l][r][k + 1];//同理
for(int h = l; h <= r; h ++) {
int val = dp[l][h - 1][k] + dp[h + 1][r][k] + xs[k - 1] * cnt[h][k];
if(val >= dp[l][r][k]) dp[l][r][k] = val, opt[l][r][k] = h, Min[l][r][k] = k;
}
}
Periodni
\qquad 题面
\qquad 笛卡尔树 d p dp dp。
\qquad 我们可以建立一个小根笛卡尔树,然后做树形 d p dp dp。这么想的原因是笛卡尔树上两棵不同的子树(没有祖先关系)间没有影响,可以分开 d p dp dp。建出笛卡尔树之后正常做树背包即可。
\qquad 核心 C o d e Code Code:
void dfs(int x, int fa) {
if(l[x]) dfs(l[x], x);
if(r[x]) dfs(r[x], x);
sze[x] = sze[l[x]] + sze[r[x]] + 1;
for(int i = 0; i <= sze[l[x]]; i ++)
for(int j = 0; j <= sze[r[x]]; j ++)
tmp[x][i + j] = (tmp[x][i + j] + (1LL * dp[l[x]][i] * dp[r[x]][j]) % mod) % mod;
for(int i = 0; i <= sze[x]; i ++)
for(int j = 0; j <= i; j ++)
dp[x][i] = (dp[x][i] + (1LL * (1LL * (1LL * C(sze[x] - j, i - j) * C(h[x] - h[fa], i - j)) % mod * fac[i - j]) % mod * tmp[x][j]) % mod) % mod;
}
[AGC026D] Histogram Coloring
\qquad 题面
\qquad 跟上一题同种套路。不过本题需要注意:如果当前行是严格的黑白相间,那么下一行可以选择反色或复制两种;但如果不是,下一行就只能反色。转移时多记录一个 0 / 1 0/1 0/1 表示是否黑白相间即可。
\qquad 核心 C o d e Code Code:
inline LL H(int l, int r, int opt, int d) {//把一个状态压成一个数
return d + 1LL * n * opt + 2LL * n * r + 2LL * n * n * l;
}
int dfs(int l, int r, int opt, int d) {//记忆化搜索
if(l > r) return 1;
LL sta = H(l, r, opt, d);
if(l == r) return mp[sta] = opt ? 0 : qpow(2, h[l] - d);
if(mp[sta]) return mp[sta];
int loc = m[l][r];
if(!opt) return mp[sta] = (1LL * (1LL * dfs(l, loc - 1, 0, h[loc]) * dfs(loc + 1, r, 0, h[loc])) % mod * qpow(2, h[loc] - d)) % mod;
if(loc == l) return mp[sta] = (2LL * (dfs(l + 1, r, 0, h[loc]) + dfs(l + 1, r, 1, h[loc])) % mod) % mod;
if(loc == r) return mp[sta] = (2LL * (dfs(l, r - 1, 0, h[loc]) + dfs(l, r - 1, 1, h[loc])) % mod) % mod;
int res1 = (6LL * (1LL * dfs(l, loc - 1, 0, h[loc]) * dfs(loc + 1, r, 0, h[loc])) % mod) % mod;
int res2 = (4LL * (1LL * dfs(l, loc - 1, 0, h[loc]) * dfs(loc + 1, r, 1, h[loc])) % mod) % mod;
int res3 = (4LL * (1LL * dfs(l, loc - 1, 1, h[loc]) * dfs(loc + 1, r, 0, h[loc])) % mod) % mod;
int res4 = (2LL * (1LL * dfs(l, loc - 1, 1, h[loc]) * dfs(loc + 1, r, 1, h[loc])) % mod) % mod;
return mp[sta] = (((res1 + res2) % mod + res3) % mod + res4) % mod;
}
[JOI Open 2016] 摩天大楼
\qquad 见插入类 dp 总结。
[USACO19DEC] Tree Depth P
\qquad 题面
\qquad 先不考虑深度和,只考虑如何做求长度为 n n n 的全排列中逆序对数为 K K K 的方案数。我们考虑设 d p i , j dp_{i,j} dpi,j 表示前 i i i 个数,逆序对数为 j j j 的方案数。转移时考虑第 i i i 个数插入后可能会产生 [ 0 , i − 1 ] [0,i-1] [0,i−1] 个逆序对,所以大力转移,加个前缀和优化可以搞到 O ( n 3 ) O(n^3) O(n3)。现在再想这个题。深度之和相当于找 i i i 祖先的个数。那么我们可以枚举 u , v u,v u,v 表示当前点是 u u u, v v v 是 u u u 的祖先的方案数。对于每一个相同的 u − v u-v u−v,答案都是相同的,所以我们先求出原始 d p dp dp 数组,然后枚举 u − v u-v u−v,在 O ( k ) O(k) O(k) 时间复杂度内做撤销背包即可。注意 u , v u,v u,v 的先后顺序不确定,若 v v v 在前则产生 0 0 0 个逆序对,若 v v v 在后则产生 v − u v-u v−u 个逆序对。
\qquad 核心 C o d e Code Code:
void Insert(int x) {//前缀和优化,但是先加后删
for(int i = 1; i <= n * (n - 1) / 2; i ++) dp[i] = (dp[i] + dp[i - 1]) % mod;
for(int i = n * (n - 1) / 2; i > x; i --) dp[i] = (dp[i] - dp[i - x - 1] + mod) % mod;
}
void Erase(int x) {
for(int i = x + 1; i <= n * (n - 1) / 2; i ++) dp[i] = (dp[i] + dp[i - x - 1]) % mod;
for(int i = n * (n - 1) / 2; i; i --) dp[i] = (dp[i] - dp[i - 1] + mod) % mod;
}
//main中
dp[0] = 1;
for(int i = 1; i < n; i ++) Insert(i);
for(int i = 1; i < n; i ++) {
Erase(i);
for(int j = i + 1; j <= n; j ++) {
ans[j] = (ans[j] + dp[K]) % mod;
ans[n - j + 1] = (ans[n - j + 1] + dp[n * (n - 1) / 2 - K]) % mod;
}
Insert(i);
}
for(int i = 1; i <= n; i ++) printf("%d ", (ans[i] + dp[K]) % mod);//深度是祖先数加1,所以最后要加上总方案
[BZOJ3864]–Hero meet devil
\qquad d p dp dp 套 d p dp dp 典型例题。
\qquad 考虑先预处理出 f s t a , j f_{sta, j} fsta,j 表示若当前 L C S LCS LCS 状态为 s t a sta sta,在后面添加字符 j j j 得到的新的 L C S LCS LCS 状态是什么。这一过程正常跑 L C S LCS LCS 即可。然后再设 d p i , s t a dp_{i, sta} dpi,sta 表示长度为 i i i 的字符串, L C S LCS LCS 的状态为 s t a sta sta 的方案数。转移很简单。
\qquad 核心 C o d e Code Code:
int solve(int sta, int x) {//当LCS状态为sta时,若在后面添加字符x,产生的新的LCS状态 预处理f的函数
for(int i = 1; i <= n; i ++) h[i] = h[i - 1] + ((sta >> (i - 1)) & 1);
memset(g, 0, sizeof g);
for(int i = 1; i <= n; i ++) {
g[i] = max(g[i - 1], h[i]);
if(Get(s[i]) == x) g[i] = max(g[i], h[i - 1] + 1);
}
int mask = 0;
for(int i = 1; i <= n; i ++) mask += (g[i] - g[i - 1]) * (1 << (i - 1));
return mask;
}
//main中
dp[0][0] = 1;
for(int i = 0; i < m; i ++) {
for(int j = 0; j < (1 << n); j ++) {
for(int k = 0; k < 4; k ++) {
dp[i + 1][f[j][k]] = (dp[i + 1][f[j][k]] + dp[i][j]) % mod;
}
}
}
for(int i = 0; i < (1 << n); i ++) ans[num[i]] = (ans[num[i]] + dp[m][i]) % mod;
LOJ 6274 - 数字
\qquad 题面
\qquad
可以写成
d
p
dp
dp 套
d
p
dp
dp,但是没必要。考虑直接大力设计状态
d
p
i
,
0
/
1
,
0
/
1
,
0
/
1
,
0
/
1
dp_{i,0/1,0/1,0/1,0/1}
dpi,0/1,0/1,0/1,0/1 表示考虑到了第
i
i
i 位,目前
x
x
x 是否贴上下界,
y
y
y 是否贴上下界。转移时有小细节:若当前位
i
&
j
=
0
i\&j=0
i&j=0,那么将会有三种情况:
(
1
,
0
)
,
(
1
,
1
)
,
(
0
,
0
)
(1,0),(1,1),(0,0)
(1,0),(1,1),(0,0)。显然不能都算入。上结论:最终方案数要从中取
max
\max
max。证明不会
\qquad 核心 C o d e Code Code:
LL dfs(int x, int o1, int o2, int o3, int o4) {
if(x < 0) return 1;
if(f[x][o1][o2][o3][o4] != -1) return f[x][o1][o2][o3][o4];
int lx = ((Lx >> x) & 1), ly = ((Ly >> x) & 1), rx = ((Rx >> x) & 1), ry = ((Ry >> x) & 1), t = ((T >> x) & 1);
LL Max = 0, ans = 0;
for(int i = 0; i < 2; i ++) {
for(int j = 0; j < 2; j ++) {
if(o1 && i < lx) continue;
if(o2 && i > rx) continue;
if(o3 && j < ly) continue;
if(o4 && j > ry) continue;
if((i | j) != t) continue;
int p1 = (o1 & (i == lx)), p2 = (o2 & (i == rx)), p3 = (o3 & (j == ly)), p4 = (o4 & (j == ry));
if(i & j) ans += dfs(x - 1, p1, p2, p3, p4);
else Max = max(Max, dfs(x - 1, p1, p2, p3, p4));
}
}
return f[x][o1][o2][o3][o4] = ans + Max;
}
Yet Another Minimization Problem
\qquad 题面
\qquad 四边形不等式优化板子题。采用分治+莫队写法即可。
\qquad Code
[USACO19FEB] Mowing Mischief P
\qquad 题面
\qquad 第一步:求出二维 L I S LIS LIS:所有点按照 x x x 从小到大排序,然后做 y y y 的最长上升子序列。树状数组优化可以搞到 O ( n log T ) O(n\log T) O(nlogT)。第二部:在 L I S LIS LIS 求出来的基础上最小化覆盖面积。不难想到按照 L I S LIS LIS 分层。分层后不难发现同一层中的点一定是 x x x 单增, y y y 单减的。按照层作为阶段进行 d p dp dp。设 d p i dp_i dpi 表示必须经过第 i i i 朵花的最小覆盖面积。那么 d p i = m i n x i ≥ x j , y i ≥ y j , l i s i = l i s j + 1 ( d p j + ( x i − x j ) × ( y i − y j ) ) dp_i=min_{x_i\geq x_j,y_i\geq y_j,lis_i=lis_j+1}(dp_j+(x_i-x_j)\times(y_i-y_j)) dpi=minxi≥xj,yi≥yj,lisi=lisj+1(dpj+(xi−xj)×(yi−yj))。先考虑如果没有 x i ≥ x j , y i ≥ y j x_i\geq x_j,y_i\geq y_j xi≥xj,yi≥yj 的限制怎么搞?直接上决策单调性优化即可。不过要注意这个决策单调性是反着的,后面的点对前面的更优。现在考虑加上这个限制怎么搞?考虑把决策单调性优化搬到线段树上,用线段树分治来搞。
\qquad 核心 C o d e Code Code:
void calc(int x, int l, int r, int ql, int qr, int k) {
if(l > r || ql> qr) return ;
int mid = l + r >> 1, pos; LL ans = 0x3f3f3f3f3f3f3f3f;
for(int i = ql; i <= qr; i ++) {
LL val = dp[loc[k][i]] + 1LL * (a[qs(x)[mid]].x - a[loc[k][i]].x) * (a[qs(x)[mid]].y - a[loc[k][i]].y);
if(val < ans) ans = val, pos = i;//注意这里不能直接带dp数组,因为dp数组会被多次更新
}
dp[qs(x)[mid]] = min(dp[qs(x)[mid]], ans);
calc(x, l, mid - 1, pos, qr, k), calc(x, mid + 1, r, ql, pos, k);//决策单调性是反的
}
Modest Substrings
\qquad 题面
\qquad 先想一手暴力怎么搞?考虑把 [ l , r ] [l,r] [l,r] 内的所有串插入 A C AC AC 自动机内然后暴力 d p dp dp 即可。但是显然不行。此时,我们注意到,假设 [ l , r ] = [ 100 , 200 ] [l,r]=[100,200] [l,r]=[100,200],那么当我们填数填到 11 _ 11\_ 11_ 的时候,最后一位上的 A C AC AC 自动机就没必要建出来了,因为此时一定在合法区间内。所以按照这个建法建出 A C AC AC 自动机后正常跑 d p dp dp 即可。需要预处理一个 g i , j g_{i,j} gi,j 表示当前在 A C AC AC 自动机的节点 i i i 上,往后跳 j j j 步能得到的最大权值。给 d p dp dp 数组转移时加个前缀和优化即可。因为要输出方案,所以在求出 d p dp dp 数组后倒推一遍是从哪转移过来的即可。
\qquad Code
[AGC022E] Median Replace
\qquad 题面
\qquad 先考虑如果没有 ? ? ?,如何判断一个 01 01 01 串能否变成一个 1 1 1。我们考虑维护一个栈。如果遇到 0 0 0 且栈顶已经有两个 0 0 0,那我们直接把这 3 3 3 个 0 0 0 合并成一个 0 0 0 即可;否则直接插入栈中。如果遇到 1 1 1 且栈顶是 0 0 0,那么我们可以同时删去这对 1 , 0 1,0 1,0,因为此时中位数是什么只会取决于后面插入内个数,与它们已经无关了。否则直接插入栈中。此时,我们发现最终的栈一定是前面一段连续 1 1 1,后面有 0 ∼ 2 0\sim 2 0∼2 个 0 0 0。此时,我们关注一下极限情况:若栈中是 11100 11100 11100,那么我们仍然可以将栈中的数合并为一个 1 1 1,所以栈中 1 1 1 的个数也可以压缩到 0 ∼ 2 0\sim 2 0∼2个,只要最终栈中 1 1 1 的个数大于 0 0 0 的个数即可。
\qquad 所以我们根据上述结论设计 d p dp dp 状态:设 d p i , 0 ∼ 2 , 0 ∼ 2 dp_{i,0\sim 2,0\sim 2} dpi,0∼2,0∼2 表示前 i i i 个数,栈中有 0 ∼ 2 0\sim 2 0∼2 个 1 1 1, 0 ∼ 2 0\sim 2 0∼2 个 0 0 0 的方案数。对于 s i ≠ ? s_i≠? si=?,分两种情况分别转移;对于 s i = ? s_i=? si=?,两种情况同时转移即可。
\qquad Code