动态规划 刷题记录

本文最后更新于 2024年10月16日 上午

AT_DP_C Vacation

洛谷 link

NN 天,对于每一天 ii1iN1 \leq i \leq N),可以选择以下活动之一:

  • A:在海里游泳,获得幸福度 aia _ i
  • B:在山上抓虫,获得幸福度 bib _ i
  • C:在家做作业,获得幸福度 cic _ i

不能连续两天以上做同样的活动,计算最大总幸福度。

DP 板子题。设 fi,jf_{i,j} 表示截止到第 ii 天时做第 jj 件事的幸福值总和

则易得转移方程为:

fi,1=max(fi1,2,fi1,3)fi,2=max(fi1,1,fi1,3)fi,3=max(fi1,1,fi1,2)f_{i,1}=\max(f_{i-1,2},f_{i-1,3})\\ f_{i,2}=\max(f_{i-1,1},f_{i-1,3})\\ f_{i,3}=\max(f_{i-1,1},f_{i-1,2})

Core Code:

1
2
3
4
5
6
for(int i=1;i<=n;i++)
{
f[i][1]=max(f[i-1][2],f[i-1][3])+a[i];
f[i][2]=max(f[i-1][1],f[i-1][3])+b[i];
f[i][3]=max(f[i-1][1],f[i-1][2])+c[i];
}

AT_DP_K Stones

洛谷 link

NN 个正整数组成的集合 A={a1,a2,,aN}A = \{ a _ 1, a _ 2, \ldots, a _ N \}。太郎君和次郎君将用以下游戏进行对决。

首先,准备一个有 KK 个石子的堆。两人依次进行以下操作。太郎君先手。

  • 从集合 AA 中选择一个元素 xx,从石堆中恰好移除 xx 个石子。

不能进行操作的人输掉游戏。当两人都按照最优策略行动时,判断谁会获胜。

显然当一名玩家操作完成后石子数量为 00,则这名玩家必胜。

fif_i 表示剩余 ii 枚石子当前操作的人的输赢情况 (1/01/0),可以发现当且仅当当前操作的上一步操作必输,当前操作才可以必胜,即如果有 aja_j 使得 fiaj=0f_{i-a_j}=0,则 fi=1f_i=1

初始化 f0=0f_0=0,则有:

fi={1,aji,fiaj=00,otherwise.f_i=\begin{cases} 1,&a_j\leq i,f_{i-a_j}=0\\ 0 ,& \text{otherwise.} \end{cases}

Core Code:

1
2
3
4
5
6
7
8
f[0]=0;
for(int i=1;i<=k;i++)
{
for(int j=1;j<=n;j++)
{
if(i-a[j]>=0 && !f[i-a[j]]) f[i]=1;
}
}

AT_DP_M Candies

link

tag: 背包 DP, 前缀和

KK 颗糖分给 nn 个人,第 ii 个人至少分得 00 颗,至多分得 aia_i 颗,必须分完,求方案数,答案对 109+710^9+7 取模。

类似背包的处理思路,设 fi,jf_{i,j} 表示第 ii 个人分完后已经分了 jj 个糖,它可以由上一步的 (jai)j(j-a_i)\sim j 转移得到,所以

fi,j=k=max(0,jai)jfi1,kf_{i,j}=\sum^j_{k=\max(0,j-a_i)}f_{i-1,k}

考虑优化时间,滚动数组不可行。发现每一个 ii 对应的值都由 i1i-1 中连续一段的值转移而来,想到用滚动的前缀和维护 i1i-1 的所有 ff 值。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int calc(int l,int r) {
if(l==0) return sum[r];
return sum[r]-sum[l-1];
}
int main() {
f[0][0]=1;
for(int i=1;i<=n;i++) {
sum[0]=f[i-1][0];
for(int j=1;j<=m;j++) {
sum[j]=(sum[j-1]+f[i-1][j]+mod)%mod;
}
for(int j=0;j<=m;j++) {
f[i][j]=(calc(max(0ll,j-a[i]),j)+mod)%mod;
}
}
printf("%lld\n",f[n][m]%mod);
}

AT_DP_P Independent Set

link

tag: 树形 DP

给一棵树,每一个点可以染成黑色或白色,任意两个相邻节点不能都是黑色,求方案数。

fu,0/1f_{u,0/1} 表示 uu 节点染成 W/B 颜色时的方案数。DFS 子树的方案数,可以得到:

fu,0=vu(fv,0+fv,1)fu,1=vufv,0\begin{aligned} f_{u,0}&=\prod_{v\in u}(f_{v,0}+f_{v,1})\\ f_{u,1}&=\prod_{v\in u}f_{v,0} \end{aligned}

注意 11 号点为根,输入无序需建双向边。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
void dfs(int u) {
g.vis[u]=1;
f[u][0]=f[u][1]=1;
for(int i=0;i<e[u].size();i++) {
int v=e[u][i];
if(!g.vis[v]) {
dfs(v);
f[u][0]=(f[u][0]*(f[v][0]+f[v][1]))%mod;
f[u][1]=(f[u][1]*f[v][0])%mod;
}
}
}

AT_DP_Q Flowers

link

有一排花,共 nn 个,第 ii 个的高度是 hih_i ,权值是 aia_i ,保证高度互不相同。现在拿走一些花,使剩下的花高度单调递增,问剩下的花权值之和最大是多少。

tag: 线段树优化 DP

带权的最长上升子序列,设 fif_i 表示第 ii 个元素结尾的最大答案,可以参照最长上升子序列写出转移方程

fi=maxj=1i1{fjhj<hi}+aif_{i}=\max_{j=1}^{i-1}\{f_{j}|h_j<h_i\}+a_{i}

考虑优化,观察到转移方程是区间最大值,想到用值域线段树优化。将 hih_i 作为线段树的下标,对应当前高度时的 fif_i

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
struct node {
ll val;
}t[N * 4];
struct tree {
void pushup(int p) {
t[p].val = max(t[p * 2].val, t[p * 2 + 1].val);
}
void modify(int p, int l, int r, int x, ll k) {
if (l == r) {
t[p].val = k;
return;
}
int mid = (l + r) >> 1;
if (x <= mid) modify(p * 2, l, mid, x, k);
else modify(p * 2 + 1, mid + 1, r, x, k);
pushup(p);
}
ll query(int p, int l, int r, int x, int y) {
if (x <= l && r <= y) {
return t[p].val;
}
ll res = 0;
int mid = (l + r) >> 1;
if (x <= mid) res = max(res, query(p * 2, l, mid, x, y));
if (y >= mid + 1) res = max(res, query(p * 2 + 1, mid + 1, r, x, y));
return res;
}
}sgt;
int main() {
n = read();
for (int i = 1; i <= n; i++) {
h[i] = read();
}
for (int i = 1; i <= n; i++) {
a[i] = read();
}
for (int i = 1; i <= n; i++) {
f[i] = sgt.query(1, 1, n, 1, h[i]) + a[i];
sgt.modify(1, 1, n, h[i], f[i]);
}
cout << sgt.query(1, 1, n, 1, n) << endl;
return 0;
}

AT_DP_T Permutation

link

有一个长为 NN 的正整数排列。给定一个由 <> 组成长为 N1N-1 的的字符串。对于任意满足 1iN11 \le i \le N-1 的字符 sis_i,如果 sis_i<Pi<Pi+1P_i<P_{i+1}、如果 sis_i>Pi>Pi+1P_i>P_{i+1}。求满足这样的性质的排列 PP 的方案数。

由于 pp 是排列,我们不在意 pp 中到底放了什么数,只需要关注当前一步放的数和上一步放的数的大小关系。可以得到状态 fi,jf_{i, j} 表示前 ii 个数形成的排列中,第 ii 个数是第 jj 大的方案数。

在转移时,若当前填的数需大于前一个数,那么直接得到 fi=k=1j1fi1,kf_i=\sum_{k=1}^{j-1}f_{i-1,k}

若小于前一个数,可以将 p1pi1p_1\sim p_{i-1}j\geq j 的数全部加 11,保证组成的是排列,这等价于遍历的排名 kk 都减一(自行手玩样例),fi=k=ji1fi1,kf_{i}=\sum_{k=j}^{i-1}f_{i-1,k}

注意到转移是求连续一段的和,前缀和优化即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
f[1][1] = 1;
for (int i = 1; i <= n; i++) {
sum[i][1] = 1;
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
if(s[i-1] == '>') {
f[i][j] = (f[i][j] + sum[i-1][i-1] - sum[i-1][j-1] + mod) % mod;
} else {
f[i][j] = (f[i][j] + sum[i-1][j-1] + mod) % mod;
}
sum[i][j] = (sum[i][j-1] + f[i][j] + mod) % mod;
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = (ans + f[n][i] + mod) % mod;
}

TDPC_IWI イウィ

洛谷 link

tag: 区间 DP

给定一个仅由字符 i\texttt{i}w\texttt{w} 构成的字符串 ss。你可以进行若干次操作,每次从串中选取连续的三个字符 iwi\texttt{iwi} 并删除;每次操作后这三个字符的左侧和右侧会连接在一起,得到一个长度比原来小 33 的新串。求可能的最大操作次数。

区间 DP,类似合并石子。

我们定义 fi,jf_{i,j} 记录 iijj 能删掉多少字符,显然最后答案应除以 33

因为答案计算的是最大的步数,那么对于一个大区间,在一般情况下的答案就应该是其分成的两个小区间的答案之和。考虑枚举这两个区间之间的分界点 kk ,答案取最大值即可。

本题还需要特判一种特殊情况。如果 sis_isjs_jisks_kw,且中间的两个小段可以被完全删除。那么,当中间的被删掉之后,左右的 i 和中间的 w 就会拼在一起,也可以删掉,那么这个区间的答案就应该是它的长度,但显然直接相加有可能会出错。

Core Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for(int a=2;a<=len;a++)
{
for(int i=1;a+i-1<=len;i++)
{
int j=a+i-1;
for(int k=i;k<j;k++)
{
f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]);
if(s[i]=='i' && s[j]=='i' && s[k]=='w' && f[i+1][k-1] == k-i-1 && f[k+1][j-1] == j-1-k)
{
f[i][j]=j-i+1;
}
}
}
}
write(f[1][len]/3);

ABC247F Cards

并查集,待填坑。

ABC336D Pyramid

link

对于正整数 kk,一个大小为 kk 的“金字塔数列”为一个长度为 2k12k-1 的数列,里面的数字依次为 1,2,3,k1,k,k1,3,2,11,2,3,\dots k-1,k,k-1,\dots 3,2,1
现在给一个长度为 nn 的数列 SS,你可以进行以下操作任意次,使得数列最后变为一个“金字塔数列”:

  • 选择一个数 i(1in)i(1 \le i \le n),把 SiS_i 减少 11
  • 删除整个数列的第一个或最后一个数字。

问最后生成的“金字塔数列”的最大的 kk 是多少。

显然一个金字塔数列是由左右两部分拼接而成的。设 fif_{i} 表示前 ii 个数能组成左半部分的最长长度,gig_{i} 表示以 ii 为首的序列能组成右半部分的最长长度(差不多等价于后 ii 个数,但注意转移方程的写法)。一个明显的性质是如果满足可以构成长度为 xx 的序列,那么一定可以通过删掉元素构成长度为 x1x-1 的序列,所以可以发现以 ii 为中间项的最长长度即为 min{fi,gi}\min\{f_i, g_i\}

ff 为例,假设 sis_i 的所有值都是 ++\infty,可以得到显然的转移方程 fi=fi1+1f_{i}=f_{i-1}+1。如果加上 sis_i 的限制条件,那么 fi=min{fi1+1,si}f_i=\min\{f_{i-1}+1, s_i\}。同理可得 gi=min{gi+1+1,si}g_i=\min\{g_{i+1}+1, s_i\}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
n = read();
for (int i = 1; i <= n; i++) {
a[i] = read();
}
f[1] = g[n] = 1;
for (int i = 2; i <= n; i++) {
f[i] = min(f[i-1] + 1, a[i]);
}
for (int i = n - 1; i >= 1; i--) {
g[i] = min(g[i+1] + 1, a[i]);
}
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, min(f[i], g[i]));
}
cout << ans << endl;

SP703 Mobile Service

Problem Link

33 个流动员工,任何时刻只有一名员工可以移动,不允许同一位置上有 22 个及以上员工。

从位置 pp 移动到位置 qq 需要花费 c(p,q)c(p,q) 的价钱,不移动不需要花费(即 c(i,i)=0c(i,i)=0 )但不保证 c(p,q)=c(q,p)c(p,q)=c(q,p)

现在给出 NN 个请求,第 ii 个请求发生在位置 xix_i。公司必须按照顺序,派一名员工到位置 xix_i,过程中不能去其他地方,也就是必须直接过去。

33 个流动员工的初始位置分别为 1,2,31,2,3

求公司的最小花费。

tag: DP

Solution

fi,x,y,zf_{i,x,y,z} 表示处理到第 ii 个请求,三个人分别在 x,y,zx,y,z 位置上时的最小值。注意到三个人中一定会有一个人在上一个请求的位置,故状态可以优化为 fi,x,yf_{i,x,y},此时令 z=qi1z=q_{i-1} 则有

fi+1,x,y=min(fi+1,x,y,fi,x,y+cz,qi+1)fi+1,x,z=min(fi+1,x,z,fi,x,y+cy,qi+1)fi+1,y,z=min(fi+1,y,z,fi,x,y+cx,qi+1)f_{i+1,x,y}=\min(f_{i+1,x,y},f_{i,x,y}+c_{z,q_{i+1}})\\ f_{i+1,x,z}=\min(f_{i+1,x,z},f_{i,x,y}+c_{y,q_{i+1}})\\ f_{i+1,y,z}=\min(f_{i+1,y,z},f_{i,x,y}+c_{x,q_{i+1}})

初值 q0=1,f0,2,3=0q_0=1, f_{0,2,3}=0

Core Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for(int i=0;i<n;i++) {
for(int x=1;x<=l;x++) {
for(int y=1;y<=l;y++) {
int z=q[i];
if(x==y || x==z || y==z) continue;
f[i+1][x][y]=min(f[i+1][x][y],f[i][x][y]+c[z][q[i+1]]);
f[i+1][z][y]=min(f[i+1][z][y],f[i][x][y]+c[x][q[i+1]]);
f[i+1][x][z]=min(f[i+1][x][z],f[i][x][y]+c[y][q[i+1]]);
}
}
}
int ans=INF;
for(int x=1;x<=l;x++) {
for(int y=1;y<=l;y++) {
int z=q[n];
if(x==y || x==z || y==z) continue;
ans=min(ans,f[n][x][y]);
}
}

P4310 绝世好题

Link 和同类题 CF264B Good Sequences

给定一个长度为 nn 的数列 aia_i,求 aia_i 的子序列 bib_i 的最长长度 kk,满足 $b_i\ \text{and}\ b_{i-1} \not =0 $,其中 2ik2\leq i\leq k

tag: 位运算, DP

Solution

fif_{i} 表示最后一个数为 aia_i 的最长长度,bitibit_i 表示遍历到当前数二进制位 ii11 的数量。

可以注意到只要两个数同一二进制位都为 11,那么他们与起来一定不等于 00。同时可以注意到上面的结论还可以传递,比如 (1000)2 and (1010)2 and (0010)20(1000)_2\ \text{and}\ (1010)_2\ \text{and}\ (0010)_2\not =0

所以在每次循环时,先通过枚举 aia_i 每一个二进制位更新 fif_i,然后用当前情况的 fif_i 去更新 aia_i 二进制为为 11bitibit_i

{fi=max(fi,bitj)bitj=fi,ai and (1<<j)0\begin{cases} f_i&=\max(f_i, bit_j)\\ bit_j&=f_i \end{cases} , a_i\ \text{and}\ (1<<j)\not =0

Core Code

1
2
3
4
5
6
7
8
9
10
11
12
for(int i=1;i<=n;i++) {
for(int j=0;j<=32;j++) {
if((1<<j)&a[i]) f[i]=max(f[i],bit[j]+1);
}
for(int j=0;j<=32;j++) {
if((1<<j)&a[i]) bit[j]=f[i];
}
}
int ans=0;
for(int i=1;i<=n;i++) {
ans=max(ans,f[i]);
}

P1944 最长括号匹配

Problem Link

对一个由 (,),[,] 括号组成的字符串,求出其中最长的括号匹配子串。

  1. (),[] 是括号匹配的字符串。
  2. 若 A 是括号匹配的串,则 (A),[A] 是括号匹配的字符串。
  3. 若 A,B 是括号匹配的字符串,则 AB 也是括号匹配的字符串。

例如:(),[],([]),()() 都是括号匹配的字符串,而 ][,[(]) 则不是。

字符串 A 的子串是指由 A 中连续若干个字符组成的字符串,包含空串。

Solution

fif_i 表示到 sis_i 时最长括号匹配,从头开始线性扫描,扫到后半括号时判断是否能组成匹配。

显然,当且仅当在其 前面括号匹配长度(fi1f_{i-1})的前一个字符是同类型的前半括号(即类似
(xxx))。

由此可得式子 fi=fi1+fifi12+2f_i=f_{i-1}+f_{i-f_{i-1}-2}+2。其中 ifi12i-f_{i-1}-2 表示前半括号。

Core Code

1
2
3
4
5
6
7
8
9
10
11
for(int i=1;i<=n;i++) {
if((s[i]==')' && s[i-f[i-1]-1]=='(') || (s[i]==']' && s[i-f[i-1]-1]=='[')) f[i]=f[i-1]+f[i-f[i-1]-2]+2;
if(ans<f[i]) {
ans=f[i];
last=i;
}
}
for(int i=last-ans+1;i<=last;i++) {
putchar(s[i]);
}
el;

P1284 三角形牧场

Problem Link

给定多个线段,求全部使用情况下最大能围成的三角形周长。

tag: 背包

Solution

显然要用海伦公式求周长,那就需要枚举三角形三条边的长度。由于所有边都需要使用,可以优化为仅枚举两条边的长度。

fk,i,jf_{k,i,j} 表示第 kk 个边时,是否存在一个两边长为 i,ji, j 的三角形。

类似 01 背包思路,fkf_k 可以由 fk1f_{k-1} 转移而来,当前的边可以加在 i,ji,j 和第三条边上,也就可以由 fk1,ilk,j,fk1,i,jlk,fk1,i,jf_{k-1, i-l_k, j}, f_{k-1, i, j-l_k}, f_{k-1, i, j} 转移而来

fk,i,j={true,fk1,ilk,j=true  fk1,i,jlk=true  fk1,i,j=truefalse,otherwise.f_{k,i,j}= \begin{cases} \text{true}&, f_{k-1,i-l_k,j}=\text{true}\ \texttt{或}\ f_{k-1,i,j-l_k}=\text{true}\ \texttt{或}\ f_{k-1,i,j}=\text{true}\\ \text{false}&, \text{otherwise.} \end{cases}

同 01 背包一样,kk 这一维可以滚动掉。

Core Code

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
bool check(int a,int b,int c) {
if(a+b>c && a+c>b && b+c>a) return 1;
else return 0;
}
double calc(double a,double b,double c) {
double p=(a+b+c)/2.0;
return 100.0*sqrt(p*(p-a)*(p-b)*(p-c));
}
int main() {
f[0][0]=1;
for(int i=1;i<=n;i++) {
for(int j=p;j>=0;j--) {
for(int k=p;k>=0;k--) {
if(j-l[i]>=0 && f[j-l[i]][k]) f[j][k]=1;
if(k-l[i]>=0 && f[j][k-l[i]]) f[j][k]=1;
}
}
}
int ans=-1;
for(int i=1;i<=p;i++) {
for(int j=1;j<=p;j++) {
if(check(i,j,p-i-j) && f[i][j]) ans=max(ans,(int)calc(i,j,p-i-j));
}
}
}

P4084 [USACO17DEC] Barn Painting G

Problem Link

给定一颗 NN 个节点组成的树,你要给每个点涂上三种颜色之一,其中有 KK 个节点已染色,要求任意两相邻节点颜色不同,求合法染色方案数。

tag: 树形 dp

Solution

树形 DP,设 fi,jf_{i,j}ii 节点颜色 jj 的方案数。

ii 已经被染色 jj 了,那显然 fi,j=1f_{i,j}=1,其他为 00;若未被染色,则 fi,j=1,j[1,3]f_{i,j}=1, j\in [1,3]

从下向上转移:

{fu,1=vson(u)(fv,2+fv,3)fu,2=vson(u)(fv,1+fv,3)fu,3=vson(u)(fv,1+fv,2)\begin{cases} f_{u,1}=\prod_{v\in son(u)} (f_{v,2}+f_{v,3})\\ f_{u,2}=\prod_{v\in son(u)} (f_{v,1}+f_{v,3})\\ f_{u,3}=\prod_{v\in son(u)} (f_{v,1}+f_{v,2}) \end{cases}

Core Code

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
#define int ll
vector<int> e[N];
int n,k;
int f[N][5];
bool is_colored[N];
bool vis[N];
void dfs(int u) {
vis[u]=1;
for(int i=0;i<e[u].size();i++) {
int v=e[u][i];
if(!vis[v]) {
dfs(v);
f[u][1]=(f[u][1]*((f[v][2]+f[v][3]+mod)%mod)+mod)%mod;
f[u][2]=(f[u][2]*((f[v][1]+f[v][3]+mod)%mod)+mod)%mod;
f[u][3]=(f[u][3]*((f[v][1]+f[v][2]+mod)%mod)+mod)%mod;
}
}
}
signed main() {
n=read();k=read();
for(int i=1;i<n;i++) {
int u=read(),v=read();
e[u].push_back(v);
e[v].push_back(u);
}
for(int i=1;i<=k;i++) {
int b=read(),c=read();
f[b][c]=1;
is_colored[b]=1;
}
for(int i=1;i<=n;i++) {
if(!is_colored[i]) f[i][1]=f[i][2]=f[i][3]=1;
}
dfs(1);
printf("%lld\n",(f[1][1]+f[1][2]+f[1][3]+mod)%mod);
return 0;
}

P2458 [SDOI2006] 保安站岗

Problem Link

给定一颗树,每个点有点权 kk,要求一条边的两个端点至少有一个被占用,求占用整棵树的代价的最小值。

tag: 树形 DP

Solution

首先不能将状态设为一个点被占或不占,这样会造成向上转移时父节点无法影响子节点。

fu,1/2/3f_{u,1/2/3} 表示 uu 节点 自身被占用/被儿子覆盖/被父亲覆盖。

首先第一种情况显然 fu,1=vson(u)min(fv,1,fv,2,fv,3)+kuf_{u,1}=\sum_{v\in son(u)}\min(f_{v,1}, f_{v,2}, f_{v,3})+k_u

对于第三种情况,由于 uu 被父亲覆盖,自身未被占用,那么就需要使 以 uu 的儿子 vv 为根的子树中的点都被覆盖。问题转化后,只有 vv 自身被占用和 vv 被儿子覆盖可以满足条件,即 fu,3=vson(u)min(fv,1,fv,2)f_{u,3}=\sum_{v\in son(u)}\min(f_{v,1}, f_{v,2})

对于第二种情况,分析方式类似第三种,首先要满足子树被覆盖。同时,要有至少一个 vv 自身被占用,才能影响到 uu。可以在上面的式子的基础上略加更改,若最小值全部被 fv,2f_{v,2} 取到,那么最后再加上所有 vvfv,1f_{v,1} 最小的一个即可。

最终答案即为 min(froot,1,froot,2)\min(f_{root,1}, f_{root,2})

Core Code

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
void dfs(int u,int fa) {
int minn=INF;
bool flag=0;
for(auto v:e[u]) {
if(v==fa) continue;
dfs(v,u);
f[u][1]+=min(f[v][1],min(f[v][2],f[v][3]));
f[u][2]+=min(f[v][1],f[v][2]);
f[u][3]+=min(f[v][1],f[v][2]);
minn=min(minn,f[v][1]-f[v][2]);
if(f[v][1]<f[v][2]) flag=1;
}
if(!flag) f[u][2]+=minn;
}
int main() {
n=read();
for(int i=1;i<=n;i++) {
int u=read();
k[u]=read();
int m=read();
f[u][1]=k[u];
for(int j=1;j<=m;j++) {
int v=read();
e[u].push_back(v);
e[v].push_back(u);
}
}
dfs(1,0);
cout<<min(f[1][1],f[1][2])<<endl;
return 0;
}

P3478 [POI2008] STA-Station

Link

给出一个 nn 个点的树,找出一个点,使以这个点为根的树所有点的深度之和最大

tag: 树形 DP, 换根 DP

Solution

换根板子。

首先考虑如何暴力,显然是以每个点为根都跑一边 dfs,时间复杂度 O(n2)\mathcal O(n^2)

一般换根 DP 题的处理思路是:

  1. 先指定一个根节点,一次 dfs 自下而上,用子节点的值更新父节点;
  2. 从上一步的根出发,dfs 转移父节点对子节点的贡献。

fuf_{u} 为以 uu 为根的深度和。运用人类智慧可以注意到 uu 节点的值可以通过其父亲转移。如下图,我们已经处理好了 22 号节点的答案,那么发现以 44 为根的时,44 的子树(包括自己)的相对深度都减少了 11,而 44 上面的三个点深度都增加了 11

P3478a.png

可以得到转移方程:

fv=fusizv×1+(nsizv)×1=fu+n2×sizvf_{v}=f_{u}-siz_{v}\times 1+(n-siz_{v})\times 1=f_u+n-2\times siz_v

其中 sizv×1siz_v\times 1 表示 44 的子树nsizvn-siz_v 表示 44 上面的点

Core Code

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
void dfs1(int u,int fa) {
g.siz[u]=1;
for(auto v:e[u]) {
if(v==fa) continue;
g.dep[v]=g.dep[u]+1;
dfs1(v,u);
g.siz[u]+=g.siz[v];
}
}
void dfs2(int u,int fa) {
for(auto v:e[u]) {
if(v==fa) continue;
f[v]=f[u]+n-2ll*g.siz[v];
dfs2(v,u);
}
}
int main() {
dfs1(1,0);
for(int i=2;i<=n;i++) {
f[1]+=g.dep[i];
}
dfs2(1,0);
ll ans=-INF,maxn=-INF;
for(int i=1;i<=n;i++) {
if(maxn<f[i]) {
maxn=f[i];
ans=i;
}
}
cout<<ans<<endl;
return 0;
}

P3047 [USACO12FEB] Nearby Cows G

Link

给你一棵 nn 个点的树,点带权,对于每个节点求出距离它不超过 kk 的所有节点权值和 mim_i

tag: 换根 DP

Solution

fu,df_{u,d} 为与 uu 节点距离小于 kk 的权值和。

两边 dfs,第一遍向上转移,计算子树对当前节点的贡献。注意到与 uu 节点距离小于 kk 的节点数可以由它的左右子树转移而来,等价于其与所有子节点距离小于 k1k-1 的节点数之和,得到 fu,d=vson(u)fv,d1f_{u,d}=\sum_{v\in son(u)} f_{v,d-1}

第二遍时计算父亲对当前节点的贡献。同样可以由一的思路 fv,d=fu,d1(vson(u))f_{v,d}=f_{u,d-1}(v\in son(u)),但是注意到这样会重复计算子树中一部分的节点,因为到 uu 距离为 dd 的点包含到 vv 距离为 d1d-1 的点,如下图的 f2,2f_{2,2} 就包含了 f3,1f_{3,1} 的贡献,所以我们简单容斥一下减去 fv,d2f_{v,d-2} 的部分。

注意在进行第二遍 dfs 时,要注意循环顺序,防止容斥后的 ff 影响未更新的。

P3047a.png

Core Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void dfs1(int u, int fa) {
for (auto v : e[u]) {
if (v == fa) continue;
dfs1(v, u);
for (int d = 1; d <= k; d++) {
f[u][d] += f[v][d-1];
}
}
}
void dfs2(int u, int fa) {
for (auto v : e[u]) {
if (v == fa) continue;
for (int d = 1; d <= k; d++) {
f[v][d] += f[u][d-1];
}
for (int d = k; d >= 2; d--) {
f[v][d] -= f[v][d-2];
}
dfs2(v, u);
}
}

CF474E Pillars

Link

  • 给定序列长度为 nn 的序列 aa,和 dd
  • 找出一个最长的 aa 的子序列 bb(设其长度为 mm),使得对于任意的 1i<m1\le i\lt m,有 bi+1bid|b_{i+1}-b_i|\ge d
  • 输出 mm 和序列 bb 在序列 aa 中每个数的下标。

tag: 动态开点线段树, 线段树优化 DP

Solution

fif_i 表示前 ii 位的最大长度,可以得到一个显然的转移方程

fi=maxbibjd{fj}+1=max1jaid or ai+djn{fj}+1\begin{aligned} f_i &= \max_{\lvert b_i - b_j\rvert \geq d}\{f_j\} + 1\\ &=\max_{1\leq j\leq a_i - d\ \text{or}\ a_i + d\leq j\leq n}\{f_j\} + 1 \end{aligned}

考虑线段树优化掉两部分区间最值。使用权值线段树,每次计算完 ii 后把 fif_i 放到对应的 aia_i 的位置即可。

数据范围过大,需动态开点或离散化。

Core Code

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
69
70
71
int n, d;
int tot = 0;
int rt;
struct pr {
int val, id;
bool operator < (pr const& x) {
return val < x.val;
}
}a[N], f[N], pre[N];
pr max(pr x, pr y) {
if (x < y) return y;
else return x;
}
struct node {
pr val;
int l, r;
}t[40 * N];
struct tree {
void pushup(int p) {
t[p].val = max(t[t[p].l].val, t[t[p].r].val);
}
void modify(int &p, int l, int r, int x, pr w) {
if (!p) p = ++tot;
if (l == r) {
if (t[p].val < w) t[p].val = w;
return;
}
int mid = (l + r) >> 1;
if (x <= mid) modify(t[p].l, l, mid, x, w);
else modify(t[p].r, mid + 1, r, x, w);
pushup(p);
}
pr query(int p, int l, int r, int x, int y) {
if (!p || y < x) return {0, 0};
if (x <= l && r <= y) {
return t[p].val;
}
int mid = (l + r) >> 1;
pr res = {-INF, 0};
if (x <= mid) res = max(res, query(t[p].l, l, mid, x, y));
if (y >= mid + 1) res = max(res, query(t[p].r, mid + 1, r, x, y));
return res;
}
}sgt;
signed main() {
n = read(); d = read();
for (int i = 1; i <= n; i++) {
a[i].val = read();
a[i].id = i;
}
pr maxn = {0, 0};
for (int i = 1; i <= n; i++) {
f[i] = max(sgt.query(rt, 1, MAXN, 1, a[i].val - d),
sgt.query(rt, 1, MAXN, a[i].val + d, MAXN));
bug(f[i].id);
f[i].val += 1;
maxn = max(maxn, {f[i].val, i});
sgt.modify(rt, 1, MAXN, a[i].val, {f[i].val, i});
}
cout << maxn.val << endl;
stack<int> st;
for (int i = maxn.id; i >= 1; i = f[i].id) {
bug(f[i].id);
st.push(i);
}
while (!st.empty()) {
cout << st.top() << " ";
st.pop();
}
return 0;
}

CF708C Centroids

Link

给定一颗树,你有一次将树改造的机会,改造的意思是删去一条边,再加入一条边,保证改造后还是一棵树。

请问有多少点可以通过改造,成为这颗树的重心?(如果以某个点为根,每个子树的大小都不大于 n2\dfrac{n}{2},则称某个点为重心)

tag: 树形 DP, 换根 DP

Solution

根据重心的定义,如果一个点 uu 不是重心,那么它的子树中一定有且仅有一个的 siz>n2siz > \lfloor\frac{n}{2}\rfloor。如果想要把 uu 变成重心,那么就需要在这个子树中找到一个更小的子树,把它接到 uu 上,要解决的是是否存在一个这样的子树可以满足条件。

  1. 如果先将一个不需要操作即可满足条件的节点钦定为根,那么这棵树的所有子树都满足 sizn2siz\leq \lfloor\frac{n}{2}\rfloor
  2. 所以,若在这棵新树上进行 DP,对于任何一个节点 uu,如果它不是重心,只能因为除了它的子树以外的点数不满足要求,即 nsizun - siz_u。我们记录一个 gug_u 表示除了 uu 子树以外的,不超过 n2\lfloor\frac{n}{2}\rfloor 的最大子树大小,这个子树就是用来切下来挂到 uu 上的。
  3. 那么,只需要判断除了上面这两部分一定合法的,剩下的那些点是否合法,即 nsizugun2n - siz_u - g_u \leq \lfloor\frac{n}{2}\rfloor 是否成立。

CF708C-example1.png CF708C-example2.png

也就是说,问题转化为求 gug_u

考虑自上而下转移,gug_u 可能来自于(faufa_u 的子树)/(gfaug_{fa_u})/(除了子树以外所有)。由于第一种情况,还要记录一个 fuf_u 表示以 uu 为根的子树中,不超过 n2\lfloor\frac{n}{2}\rfloor 的最大子树大小。我们还可以发现,在转移 gug_u 时,可能 ffauf_{fa_u} 就表示 uu 的子树,为了解决这种情况,需要记录最大值 fu,0f_{u, 0} 和次大值 fu,1f_{u, 1}

显然 fu,0f_{u, 0} 需要自下而上转移:

fu,0={fv,0,sizv>n2sizu,sizvn2f_{u, 0}= \begin{cases} f_{v, 0}&, siz_v >\lfloor\frac{n}{2}\rfloor\\ siz_u&, siz_v \leq\lfloor\frac{n}{2}\rfloor \end{cases}

可以按下面的 trick 维护次大值,为了表示方便,用 xx 表示需要被更新的数:

{fu,1=fu,0,fu,0=x,fu,0<xfu,1=x,fu,0x<fu,1\begin{cases} f_{u, 1} = f_{u, 0}, f_{u, 0} = x&, f_{u, 0} < x\\ f_{u, 1} = x&, f_{u, 0}\leq x < f_{u, 1} \end{cases}

然后即可自上而下转移 gug_u

gv=max{fu,1,fu,0=sizvfu,0,fu,0sizvgunsizv,nsizvn2g_v=\max \begin{cases} f_{u, 1}&, f_{u, 0} = siz_v\\ f_{u, 0}&, f_{u, 0} \neq siz_v\\ g_u\\ n - siz_v&, n - siz_v \leq \lfloor\frac{n}{2}\rfloor \end{cases}

Core Code

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
void findrt(int u, int fa) {
for (auto v : e[u]) {
if (v == fa) continue;
findrt(v, u);
siz[u] += siz[v];
if (siz[v] > n / 2) return;
}
if (n - siz[u] > n / 2) return;
root = u;
}
void dfs1(int u, int fa) {
for (auto v : e[u]) {
if (v == fa) continue;
dfs1(v, u);
siz[u] += siz[v];
if (siz[v] > n / 2) {
if (f[v][0] > f[u][0]) {
f[u][1] = f[u][0];
f[u][0] = f[v][0];
} else if (f[v][0] > f[u][1]) {
f[u][1] = f[v][0];
}
} else {
if (siz[v] > f[u][0]) {
f[u][1] = f[u][0];
f[u][0] = siz[v];
} else if (siz[v] > f[u][1]) {
f[u][1] = siz[v];
}
}
}
}
void dfs2(int u, int fa) {
for (auto v : e[u]) {
if (v == fa) continue;
g[v] = max(g[v], g[u]);
if (n - siz[v] <= n / 2) {
g[v] = max(g[v], n - siz[v]);
}
if (f[u][0] == siz[v]) {
g[v] = max(g[v], f[u][1]);
} else {
g[v] = max(g[v], f[u][0]);
}
dfs2(v, u);
}
if (n - siz[u] - g[u] <= n / 2 || u == root) ans[u] = 1;
}
int main() {
n = read();
for (int i = 1; i < n; i++) {
int u = read(), v = read();
e[u].push_back(v);
e[v].push_back(u);
}
for (int i = 1; i <= n; i++) siz[i] = 1;
findrt(1, 0);
for (int i = 1; i <= n; i++) siz[i] = 1;
dfs1(root, 0);
dfs2(root, 0);
for (int i = 1; i <= n; i++) {
cout << ans[i] << " ";
}
el;
}

动态规划 刷题记录
https://blog.makerlife.top/post/dp-problems/
作者
Makerlife
发布于
2024年10月11日
更新于
2024年10月16日
许可协议