DP 入门

Arahc 11.0

这些天搞了搞 DP 专题,把一些动态规划的东西总结一下。

PS:在原本的博客站里我分成了两个文章,现在决定把它们合并。

状态设计

DP+DP

考虑一个问题,如果不能简单地用一个 DP 去解决,我们可以再来一个。

当然还有的 DP+DP 的目的是平衡复杂度,在后面会有的。

Luogu3188 梦幻岛宝珠

给定 nn 个物品,每个物品有重量和价值。一个物品只能选一次,求限重 mm 的情况下,能装载的物品价值和的最大值。

n100,m231n\leq100,m\leq 2^{31},每个重量能写成 a×2b(a10,b30)a\times 2^b(a\leq10,b\leq30) 的形式。

题解

考虑 nn 的值很小。

bb 相同的物品可以跑朴素的 01 背包,求 fi,jf_{i,j} 表示 b=ib=i,容量为 jj 的最大价值。

然后考虑合并答案。设 gi,jg_{i,j} 表示当前考虑到 b=ib=i,且 b=ib=i 的物品占用空间为 jj 时,b[0,i)b\in[0,i) 的部分空间不超过容量第 ii 位的最大价值。

转移考虑从 jj 里面拆掉一个 kk 给其他物品放:

gi,jmax(gi,j,fi,jk+gi1,min(10n,2k+(m>>(i1))&1))g_{i,j} \gets \max(g_{i,j} , f_{i,j-k} + g_{i-1,\min(10n,2k+(m>>(i-1))\&1)})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for(register int i=1;i<=n;++i){
int x=read(),cnt=x&-x;
int id=getbitnum(cnt);
w[id][++num[id]]=x/cnt;
v[id][num[id]]=read();
}
int up=get(W);
for(register int i=0;i<=up;++i)
for(register int j=1;j<=num[i];++j)
for(register int k=500;k>=w[i][j];--k)
dp[i][k]=max(dp[i][k],dp[i][k-w[i][j]]+1LL*v[i][j]);
for(register int i=0;i<=500;++i)
f[0][i]=dp[0][i];
for(register int i=1,nw=1;i<=up;++i,nw^=1)
for(register int j=0;j<=500;++j){
f[nw][j]=0;
for(register int k=0;k<=j;++k){
f[nw][j]=max(f[nw][j],
dp[i][j-k]+f[nw^1][min(10*n,(k<<1)+((W>>(i-1))&1))]);
}
}

容斥原理

用容斥进行的 DP 不少了,还是一句话,正难则反。

基本上有两大类:

  • 直接转化为全集-补集,用 DP 求解补集问题。
  • 以常规的容斥原理进行 DP,或用二项式定理、容斥原理等合并 DP 的答案。

通常我们建立的求的东西和答案所需的东西为二项式反演的关系,组合数学里有时是斯特林反演的关系,毒瘤出题人可能会把它放在计算几何里形成圆反演的关系。

上一个经典问题:

Luogu4859 已经没有什么好害怕的了

给定两个序列 a,ba,b,两序列无重复元素,无交。求将两个序列两两配对,使得 ai>bja_i>b_j 的组数比 ai<bja_i<b_j 的组数恰好多 KK 的配对方案数。

n2000n\leq 2000

题解

Kn+K2K\gets \frac{n+K}{2},则我们只需要让 ai>bja_i > b_j 的组数为 KK 即可。先将两个数组排序。

若一个 aia_i 匹配到了比它小的 bjb_j,则对于任意 j>ij>i,它匹配比它小的 bkb_k 的方案数会 1-1

这种情况貌似还算好考虑:设计状态 fi,jf_{i,j} 表示考虑到第 ii 位,已经产生了 jj 个上述情况的方案数,设 cic_i 表示 bb 序列里比 aia_i 小的数有多少个,有:

fi,j=fi1,j+(cij+1)fi1,j1f_{i,j} = f_{i-1,j}+ (c_i-j+1) f_{i-1,j-1}

剩下的数字是不是任意匹配?那如何得到恰好 KK 组的答案?这是不好算的,但是至少 KK 组的答案呢?

gig_i 表示将 nin-i 个没有按上述方法匹配的数任意分配,得到至少 ii 组合法匹配的方案数。不难有 gi=(ni)!fn,ig_i = (n-i)!f_{n,i}

然后容斥答案即可:

ans=i=Kn(1)iK(iK)gians = \sum_{i=K}^n (-1)^{i-K} \binom{i}{K} g_i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for(register int i=1;i<=n;++i)
for(register int j=0;j<=i;++j){
dp[i][j]=dp[i-1][j];
if(j && l[i]-j+1>0){
dp[i][j]+=(l[i]-j+1)*dp[i-1][j-1]%mod,
dp[i][j]%=mod;
}
}
for(register int i=0;i<=n;++i)
f[i]=dp[n][i]*fac[n-i]%mod;
for(register int i=k;i<=n;++i){
ans+=((i-k)&1?-1:1)*C(i,k)*f[i]%mod,
ans=(ans+mod)%mod;
}

DP of DP

这是一个神奇的思想。

设计动态规划的时候,从另外一个动态规划入手,考虑另外一个动态规划的转移,挖掘性质,然后建立一个动态规划来维护这个动态规划的转移。

有时候还需要我们会建一个自动机,然后在自动机上跑 DP。

BZOJ3864 Hero meet devil

给定一个由 AGCT 组成的字符串 SS,对于每个 0iS0\leq i\leq \left|S\right|,求有多少个长度为 mm 的字符串 TT,满足 S,TS,T 的最长公共子序列为 ii

S15,m1000\left|S\right|\leq15,m\leq 1000

题解

考虑如何求两个字符串的 LCS,这是一个非常经典的 DP:设 dpi,jdp_{i,j} 表示考虑到 Si,TjS_i,T_j,最长公共子序列的长度:

dpi,j=max{dpi1,j1+1(Si=Ti)dpi,j1dpi1,jdp_{i,j} = \max\begin{cases}dp_{i-1,j-1} +1 & (S_i = T_i) \\dp_{i,j-1} \\dp_{i-1,j}\end{cases}

考虑这个 DP 有什么特殊性质,我们发现 dpi,jdpi,j1{0,1}dp_{i,j}-dp_{i,j-1} \in \{0,1\}

dpidp_i 差分,用差分数组当状态。设 fi,sf_{i,s} 表示长度为 ii 的字符串与 SS 的 LCS 的 DP 匹配状态为 ss 的方案数。

实际转移前需要预处理出每个状态添加一个字符能转移到哪些状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for(register int i=0;i<S;++i){
for(register int j=1;j<=n;++j)
f[j]=f[j-1]+((i>>(j-1))&1);
for(register int k=0;k<4;++k){
for(register int j=1;j<=n;++j){
g[j]=max(g[j-1],f[j]);
if(k==s[j])
g[j]=max(g[j],f[j-1]+1);
}
to[i][k]=0;
for(register int j=1;j<=n;++j)
if(g[j]-g[j-1])
to[i][k]|=(1<<(j-1));
}
}
memset(dp,0,sizeof(dp)),
memset(ans,0,sizeof(ans));
dp[0][0]=1;
for(register int i=1,nw=1;i<=m;++i,nw^=1){
memset(dp[nw],0,sizeof(dp[nw]));
for(register int st=0;st<S;++st)
for(register int j=0;j<4;++j)
dp[nw][to[st][j]]=(dp[nw][to[st][j]]+dp[nw^1][st])%mod;
}

状态数优化

顾名思义,就是挖掘题目的性质,考虑如何优化状态数。

状态数优化还有一种精妙的实现方式:如果原 DP 的一维值域很大,但是 DP 值很小,考虑交换 DP 维度和答案。也可以实现状态数的优化。

当然更多的状态数优化还是如下几种:

  1. 从根本上优化:去除一个或几个维度。
  2. 挖掘性质,去除冗余状态。
  3. 离散、压缩、合并状态。

CF643E Bear and Destroying Subtrees

一个树,初始只有根。支持添加叶子或查询如下问题的答案:

每条边若有 50%50\% 的概率断裂,则断边后点 xx 的子树的期望高度。

误差不超过 10610^{-6}Q5×105Q\leq 5\times 10^5

题解

考虑朴素 DP 有:fi,jf_{i,j} 表示 ii 为根的子树内,断边后深度为 jj 的概率。转移是 naive 的。每次加入一个叶子的时候,需要从这个叶子一直跳到根,更新这条链上所有点的状态。更新状态是 O(1)\mathcal O(1) 的。

转移复杂度非常优秀,考虑如何优化状态。注意到精度要求为 10610^{-6},因此对于一个树,断边后高度的期望值其实不大。

例如如果断边后高度达到 100100,即使有 5×1055\times 10^5 个点,概率也只有 103110^{-31},对答案几乎没有影响。这些状态几乎是冗余的。

因此状态的第二维可以开得比较小,每次加入叶子往上也不需要跳到根,跳到某一级祖先就够了。

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
#include<bits/stdc++.h>
#define int long long
using namespace std;
bool Begin;
const int max_n=500005;
inline int read(){
int x=0;bool w=0;char c=getchar();
while(c<'0' || c>'9') w|=c=='-',c=getchar();
while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar();
return w?-x:x;
}
inline void write(int x){
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10^48);
}

int n=1,fa[max_n];
const int D=100;

double f[max_n][D+5];

inline void dfs1(int u,int i){
if(!fa[u] || i>=D) return;
dfs1(fa[u],i+1);
f[fa[u]][i+1]/=(f[u][i]+1)/2.0;
}
inline void dfs2(int u,int i){
if(!fa[u] || i>=D) return;
f[fa[u]][i+1]*=(f[u][i]+1)/2.0;
dfs2(fa[u],i+1);
}

bool End;
#define File ""
signed main(){
// #ifndef ONLINE_JUDGE
// freopen(File ".in","r",stdin);
// freopen(File ".out","w",stdout);
// #endif
// cerr<<"Memory : "<<(&Begin-&End)/1024.0/1024<<"\n";
for(register int i=1;i<=D;++i)
f[1][i]=1;
for(register int Q=read();Q;--Q){
int tp=read(),x=read();
if(tp==1){
fa[++n]=x;
for(register int i=1;i<=D;++i)
f[n][i]=1;
dfs1(x,1),dfs2(n,0);
}
else{
double ans=D;
for(register int i=1;i<D;++i)
ans-=f[x][i];
printf("%.10lf\n",ans-1);
}
}
return 0;
}

HDU4623 Crime

求有多少大小为 nn 的排列满足相邻两个数互质。

n28n\leq 28

题解

暴力 DP 设 fi,sf_{i,s} 表示最后一个数填的是 ii,已经填了集合 ss 中的数,状态数为 228×282^{28}\times 28。太大了。不能通过。

注意到我们并不需要知道这个数具体是什么,只需要让其和上一个数不互质即可。

对每个数按照其有的质因子分类,注意到 1,17,19,231,17,19,23 和任何数都互质,可以单独划分到一类。

然后你就有了 1414 类数,预处理每一类数的互质情况,设 fi,sf_{i,s} 表示最后一个数是 ii 类,每个类剩余数字情况为 ss 的方案数,则状态数为 14×172800014\times 1728000,可以通过。

这题最好写变进制状压,不要学我暴力压然后写了个高达十几维的 DP。

1
2
3
4
5
6
7
8
9
10
11
12
13
for(register int i=0;i<14;++i)
f[i==0][i==1][i==2][i==3][i==4][i==5][i==6][i==7][i==8][i==9][i==10][i==11][i==12][i==13][i]=1;
for(I[0]=0;I[0]<=up[0];++I[0]) for(I[1]=0;I[1]<=up[1];++I[1]) for(I[2]=0;I[2]<=up[2];++I[2]) for(I[3]=0;I[3]<=up[3];++I[3])
for(I[4]=0;I[4]<=up[4];++I[4]) for(I[5]=0;I[5]<=up[5];++I[5]) for(I[6]=0;I[6]<=up[6];++I[6]) for(I[7]=0;I[7]<=up[7];++I[7])
for(I[8]=0;I[8]<=up[8];++I[8]) for(I[9]=0;I[9]<=up[9];++I[9]) for(I[10]=0;I[10]<=up[10];++I[10]) for(I[11]=0;I[11]<=up[11];++I[11])
for(I[12]=0;I[12]<=up[12];++I[12]) for(I[13]=0;I[13]<=up[13];++I[13]) for(register int i=0;i<14;++i) if(I[i])
for(register int j=0;j<14;++j) if(!conf[i][j] && I[j]<up[j]){
int &to=f[I[0]+(j==0)][I[1]+(j==1)][I[2]+(j==2)][I[3]+(j==3)][I[4]+(j==4)][I[5]+(j==5)][I[6]+(j==6)][I[7]+(j==7)][I[8]+(j==8)][I[9]+(j==9)][I[10]+(j==10)][I[11]+(j==11)][I[12]+(j==12)][I[13]+(j==13)][j];
to=(to+f[I[0]][I[1]][I[2]][I[3]][I[4]][I[5]][I[6]][I[7]][I[8]][I[9]][I[10]][I[11]][I[12]][I[13]][i])%mod;
}
int ans=0;
for(register int i=0;i<14;++i)
ans=(ans+f[up[0]][up[1]][up[2]][up[3]][up[4]][up[5]][up[6]][up[7]][up[8]][up[9]][up[10]][up[11]][up[12]][up[13]][i])%mod;

转移优化

所谓转移优化,你先得要有一个暴力的转移。

然后根据观察方程/打表找性质等方法选择合适的优化。如果优化不了,反思一下是不是有更好的状态设计。

决策单调性

决策单调性,顾名思义,就是在转移过程中,使 dp 数组有最优转移的下标具有某些性质(一般是单调性),从而可以用此优化 DP。

通常证明一个决策单调性非常复杂,而且一般你也想不到这个题有决策单调性。因此考场上常用的找决策单调性的做法为打表观察。

ff 为状态数组,pp 为最优转移的决策,常见的决策单调性有两种:

  1. pp 单调。
  2. 四边形不等式。

a1a2<b1b2,fa1,b1+fa2,b2fa1,b2+fa2,b1a_1 \leq a_2 < b_1 \leq b_2, f_{a_1,b_1}+f_{a_2,b_2} \leq f_{a_1,b_2}+f_{a_2,b_1},则 ff 满足四边形不等式。

ff 满足四边形不等式,则 pi,j1pi,jpi+1,jp_{i,j-1}\leq p_{i,j}\leq p_{i+1,j}

【数据删除】

给定两个 n×nn\times n 的网格图,横向边都是向右的。已知第一个图左侧第 ii 个点到右侧第 jj 个点的最短路 ai,ja_{i,j} 和第二个图左侧第 ii 个点到右侧第 jj 个点的最短路 bi,jb_{i,j}

现在要把两个图拼成 n×2nn\times 2n 的网格图,求拼接后左侧第 ii 个点到右侧第 jj 个点的最短路 ci,jc_{i,j}

n1000n\leq 1000

题解

暴力 Floyd,ci,j=min(ai,k+bk,j)c_{i,j} = \min(a_{i,k}+b_{k,j}),复杂度为三次方。题目说是网格图,但是图本身并没有给我们,所以边权之类的也找不到性质。

如果没什么思路的话,不妨先写出来,打个表观察决策吧?不难发现,设 pi,jp_{i,j} 表示使 ci,jc_{i,j} 最小的决策点,打表发现 pi,jpi,j1p_{i,j}\geq p_{i,j-1}。为什么?

pic-1

  • len(x,P)=len(y,P)\rm{len}(x,P) = \rm{len}(y,P),对答案无影响。
  • len(x,P)<len(y,P)\rm{len}(x,P) < \rm{len}(y,P)BB 的最优决策在 xx
  • len(x,P)>len(y,P)\rm{len}(x,P) > \rm{len}(y,P)AA 的最优决策在 yy

考虑每次转移的时候,假设要求出 flrf_{l\cdots r} 的值,且已经知道了 LplprRL\leq p_l\leq p_r\leq R,则直接二分出 midmid,求出 fmid,pmidf_{mid}, p_{mid},求可以递归了。

复杂度 n2lognn^2\log n

斜率优化

可以大致分为两种:

  1. 转移方程写成向量点积的形式:fi=min(Aj+BiCj+Di)f_i = \min(A_j + B_iC_j + D_i)
  2. 转移方程可以转化为求最优斜率:fi=min(AiBjCiDj)f_i = \min(\frac{A_i-B_j}{C_i-D_j})

不同于决策单调性,判定一个 DP 是否可以斜率优化一般直接观察转移方程。

单调栈、单调队列、平衡树、分治……乱七八糟的东西都可以维护,但是因规约不同有些还不能用(比如点集的每一维都没有单调性时,不能用单调栈维护等等)。

【数据删除】

nn 个盒子,分别有 aia_i 个球,但球数和盒子无法一一对应。你可以每次选一个盒子拿一个球,如果这个盒子空了裁判会告诉你。求把 mm 个盒子取空的最坏情况下至少要拿多少球。

n2000,ai106n\leq 2000,a_i\leq 10^6

题解

显然先排序。考虑最后空盒子的集合对应在 aa 上的下标{b1,b2,,bm}\{b_1,b_2,\cdots,b_m\},则最坏情况下球数为:

i=1mabi(bibi1)\sum_{i=1}^m a_{b_i}(b_i - b_{i-1})

最优策略为:每个盒子模 ab1a_{b_1} 个球到空,剩下的盒子每个摸 ab2a_{b_2} 个球……

状态设计就简单了:fi,jf_{i,j} 表示考虑到第 ii 个盒子,空了 jj 个的最小球数:

fi,j=min(fk,j1+a1(ik))f_{i,j} = \min(f_{k,j-1} + a_1(i-k))

这个式子可以化为斜率优化通式一。单调队列优化即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inline double slp(int k1,int k2,int j){
return 1.0*(f[k2][j]-f[k1][j])/(k2-k1);
}
// ...
for(register int i=1;i<=n;++i){
for(register int j=i;j>0;--j){
if(j==1)
f[i][1]=i*a[i];
else{
while(q[j-1].size()>1 && slp(q[j-1][q[j-1].size()-2],q[j-1].back(),j-1)>=a[i])
q[j-1].pop_back();
f[i][j]=(i-q[j-1].back())*a[i]+f[q[j-1].back()][j-1];
}
while(q[j].size()>1 && slp(q[j][q[j].size()-2],q[j].back(),j)>=slp(q[j].back(),i,j))
q[j].pop_back();
q[j].push_back(i);
}
ans=min(ans,f[i][m]);
}

DS 优化

通常见到一个 DP,如果你看起来觉得它非常能 DS,那么它就可以 DS。

如果遇到一个题,它本身看起来像 DS,但它也可能是 DP+DS 优化。

有些东西看起来就不像是 DS 只像是有更优的状态/转移,它还是有可能是 DS 优化 DP。

CF1129D Isolation

给定序列 aa,将其划分为若干段,使得每段恰好出现一次的元素个数 K\leq K,求方案数。

n105n\leq 10^5

题解

这个暴力 DP 的思想就更简单了:

fi=j is okfjf_i = \sum_{\text{j is ok}} f_j

考虑怎么判 ok。记 gjg_j(j,i](j,i] 这个区间内只出现一次的数的个数。所有可转移的 jj 就是满足 gjKg_j\leq Kjj。而 gg 的维护:每次 ii 加一时相当于对 gg 进行区间加和区间减。

问题转化为:对 gg 区间 ±1\pm1,维护集合 S={igiK}S = \{i|g_i\leq K \}

考虑分块。给每个块开一个桶,整块修改打一个整体加减标记,局域修改暴力重构;整块查询直接查,局域查询暴力差即可。时间复杂度 nnn\sqrt 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
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
inline void clear(int x){
cnt[x]=0;
for(register int i=(x-1)*B+1,up=min(n,x*B);i<=up;++i){
if(!i) continue;
sm[x][g[i]]=0;
}
}
inline void build(int x){
for(register int i=(x-1)*B+1,up=min(n,x*B);i<=up;++i){
g[i]+=tag[x];
sm[x][g[i]]=(1LL*sm[x][g[i]]+f[i])%mod;
if(g[i]<=K)
cnt[x]=(1LL*cnt[x]+f[i])%mod;
}
tag[x]=0;
}
inline void pls(int x){
if(tag[x]<=K) cnt[x]=(1LL*cnt[x]-sm[x][K-tag[x]]+mod)%mod;
++tag[x];
}
inline void dec(int x){
--tag[x];
if(tag[x]<=K) cnt[x]=(1LL*cnt[x]+sm[x][K-tag[x]])%mod;
}
inline void add(int l,int r,int val){
if(l>r) return;
clear(kid[l]);
if(kid[l]==kid[r]){
for(register int i=l;i<=r;++i)
g[i]+=val;
build(kid[l]);
return;
}
for(register int i=l,up=kid[l]*B;i<=up;++i)
g[i]+=val;
build(kid[l]);
clear(kid[r]);
for(register int i=(kid[r]-1)*B+1;i<=r;++i)
g[i]+=val;
build(kid[r]);
for(register int i=kid[l]+1;i<kid[r];++i){
if(val>0) pls(i);
else dec(i);
}
}
// ...
for(register int i=1;i<=n;++i){
int prp=lst[lst[i]];
if(!prp) ++prp,g[0]-=(lst[i]>0?1:0);
add(prp,lst[i]-1,-1);
prp=lst[i];
if(!prp) ++prp,++g[0];
add(prp,i-1,1);
f[i]=(g[0]<=K?1:0);
for(register int j=1;j<=B;++j)
f[i]=(1LL*f[i]+cnt[j])%mod;
clear(kid[i]),build(kid[i]);
}

wqs 二分

如果直接设计状态发现复杂度爆炸,考虑一维是否有单调性,用 wqs 二分配合起来实现 DP 的优化。

具体而言,如果你要最优化的数和一些别的数存在凸函数关系,就可以考虑用 wqs 二分优化。这个思想可以运用在简单的二分一个维度,另一个维度用最优化类型的 DP 解决,还可以把题目转化为凸包问题求解一些奇怪的凸优化问题上。

【数据删除】

一个 nn 个点的图,每条边有两个权值 ai,bia_i,b_i。求一个生成树,最大化 (a)2+(b)2(\sum a)^2 + (\sum b)^2

n,ai,bi103n,a_i,b_i\leq 10^3

题解

貌似不好用朴素的 wqs + DP 的方法去解决它?

考虑第二个模型,如何把这个题变成一个凸包?

有凸包就要有二维平面,先整一个二维平面出来。将一棵树的 a,b\sum a,\sum b 看作横纵坐标。用 wqs 二分求出这些点的凸包。显然答案一定在凸包上。

复杂度?凸包大小不超过最大权的 23\frac{2}{3} 次幂,因此复杂度为 (na)23log(na)23(na)^{\frac{2}{3}}\log (na)^{\frac{2}{3}}

矩阵、多项式

矩阵和经典多项式的优化都见怪不怪了,一般是用于递推式子的。

如果你看到这个题的模数非常 NTT,而且转移式子非常 NTT,或者转移式非常线性递推或非常卷积,数据范围非常奇怪,可以尝试往这个方面想一想,

当然也并不是所有的多项式优化 DP 的题都这么明显。还可能用生成函数、卷积过程中的性质辅助算法的瓶颈优化。

Luogu3746 组合数问题

nknk 个不同的物品,求选出的物品数模 kkrr 的方案数。

n109,r<k50n\leq 10^9,r<k\leq 50

题解

暴力转移设 fi,jf_{i,j} 表示选到第 ii 个物品,选了的物品数量模 kkjj 的方案数:

fi,j=fi1,j+fi1,(j1)modkf_{i,j} = f_{i-1,j} + f_{i-1,(j-1)\bmod k}

显然可以用矩阵优化,观察转移方程不难得到:

[fnk,0fnk,1fnk,2fnk,3fnk,k1]=[100001110000011000001100000011]nk[10000]\begin{bmatrix}f_{nk,0} \\f_{nk,1} \\f_{nk,2} \\f_{nk,3} \\\vdots \\f_{nk,k-1}\end{bmatrix}=\begin{bmatrix}1 & 0 & 0 & 0 & \cdots & 0 & 1 \\1 & 1 & 0 & 0 & \cdots & 0 & 0 \\0 & 1 & 1 & 0 & \cdots & 0 & 0 \\0 & 0 & 1 & 1 & \cdots & 0 & 0 \\\vdots & \ddots & \ddots & \ddots & \ddots & \ddots & \vdots \\0 & 0 & 0 & 0 & \cdots & 1 & 1\end{bmatrix}^{nk}\begin{bmatrix}1 \\0 \\0 \\0 \\\vdots \\0\end{bmatrix}

复杂度 k3log(nk)k^3\log(nk)

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
struct matrix{
int a[max_n][max_n],n;
inline void init(int x,string s){
n=x;
if(s=="zero"){
memset(a,0,sizeof(a));
return;
}
if(s=="epsilion"){
memset(a,0,sizeof(a));
for(register int i=1;i<=n;++i)
a[i][i]=1;
return;
}
if(s=="init"){
memset(a,0,sizeof(a));
for(register int i=1;i<=n;++i)
++a[i][i],++a[(i==1?n:(i-1))][i];
}
}

// ...

}f,g;

signed main(){
// ...
f.init(k,"zero"),f.a[1][1]=1;
g.init(k,"init");
f=f*Pow(g,n*k);
write(f.a[1][r+1]);
}

【数据删除】

对于一个序列,令 li=max{jaj>ai,j<i},ri=max{jaj>ai,j>i}l_i = \max\{j|a_j>a_i,j<i\}, r_i = \max\{ j|a_j>a_i,j>i \}。若 lil_i 不存在,默认 00;若 rir_i 不存在,默认 n+1n+1

多组询问,每次给定 KK,查询有多少大小为 nn 的排列满足:

i=1nmin(ili,rii)=K\sum_{i=1}^n \min(i-l_i, r_i-i) = K

n200,m20n\leq 200,m\leq 20

题解

暴力设 fi,jf_{i,j} 表示长度为 ii 的排列,权值为 jj 的方案数。枚举排列最大数的位置,将其拆成两半:

fi,j=k=1ip=0jlmin(k,ik+1)0(i1k1)fk1,pfik,jpmin(k,ik+1)f_{i,j} = \sum_{k=1}^i \sum_{p=0}^{j-l-\min(k,i-k+1)\geq 0} \binom{i-1}{k-1} f_{k-1,p} f_{i-k,j-p-\min(k,i-k+1)}

状态两维转移平方,复杂度不得爆炸。貌似没有其他方法做了,考虑生成函数优化。

Fi(x)F_i(x)fif_i 的生成函数。有:

Fi=j=1i(i1j1)Fj1Fijxmin(j,ij+1)F_i = \sum_{j=1}^i \binom{i-1}{j-1} F_{j-1} F_{i-j} x^{\min(j,i-j+1)}

考虑 FiF_i 次数的范围,显然当 KnlognK\leq n\log n 时答案才不是 00。因此 FF 次数不超过 nlognn\log n,就可以卷积了。

一开始就把 FiF_i 转成点值,每次查询时用拉插还原多项式。复杂度为 n3logn+mn2log2nn^3\log n+mn^2\log^2 n

其他 Trick

随机化

对于数据规模小的时候,可以用一些诸如高幂级 DP、状压 DP 等做法解决的问题,数据规模较大时,可能可以通过随机化规约成小问题解决。

对于答案函数不具备单调性的时候,不能用朴素的二分/三分等做法,用 DP 来判定,其可能可以用模拟退火等做法模拟二分/wqs 二分,从而配合 DP 得到答案。

BZOJ5232 好题

给定一个节点带颜色的树,求最小的包含 kk 种颜色的连通块。

n104,k5n\leq 10^4,k\leq 5

题解

如果颜色数很少怎么做?

对于颜色数很少的情况,fi,sf_{i,s} 表示考虑到 ii 号点,包含这个点且颜色状态为 ss 的最小连通块大小。转移显然。

考虑颜色很多的情况,把所有颜色随机 Hash 成 [1,k][1,k] 的颜色,跑上面的做法。重复若干次取最优答案。

正确性?

显然不会求出比正解更小的解出来。而对于正解连通块,其一定恰好包含 kk 种颜色。一次 Hash 跑一遍每个颜色都可以变成 [1,k][1,k] 中任意一个数,若这些数两两不同则可以找到正解。因此一次 Hash+DP 正确的概率为 k!kk\frac{k!}{k^k}。在 k=5k=5 的情况下,这个数很小。

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
inline void dfs(int u,int fa){
f[u][1<<col[u]]=1;
for(register int i=e.hd[u];i;i=e.nx[i]){
int v=e.to[i];
if(v==fa) continue;
dfs(v,u);
for(register int j=0;j<S;++j){
if((j&(1<<col[u])) && f[u][j]!=inf)
for(register int k=0;k<S;++k)
if(f[v][k]!=inf)
f[u][j|k]=min(f[u][j|k],f[u][j]+f[v][k]);
if(__builtin_popcount(j)>=K)
res=min(res,f[u][j]);
}
}
}

inline int sol(){
for(register int i=1;i<=n;++i)
for(register int j=0,up=vec[i].size(),c=rand()%K;j<up;++j)
col[vec[i][j]]=c;
res=n;
memset(f,0x3f,sizeof(f));
dfs(1,-1);
return res;
}

复杂度平衡

这个词多用于 DS,但是 DP 也有这样的思想。具体和 DS 类似:考虑一种方法实现的转移在某方面很快,某方面很慢;另一种方式恰恰相反,则可以考虑如何将这两种方法进行结合,从而达到平衡复杂度的目的。

复杂度平衡的方式也因题而异。相对常见的比如直接将两个 DP 结合在一起。

Luogu3773 吉夫特

给定元素两两不同的序列 aa,求有多少长度不小于 22 的子序列满足:

i=2m(bi1bi)1(mod2)\prod_{i=2}^m \binom{b_{i-1}}{b_i} \equiv1 \pmod 2

n2×105,ai<222n\leq 2\times 10^5,a_i<2^{22}

题解

a=max(ai)a=\max(a_i)

考虑 Lucas 定理:

(bi1bi)(bi1mod2bimod2)(bi12bi2)(mod2)\binom{b_{i-1}}{b_i} \equiv \binom{b_{i-1}\bmod 2}{b_i\bmod 2}\binom{\frac{b_{i-1}}{2}}{\frac{b_i}{2}} \pmod 2

不难发现这是二进制拆位的过程。这个条件可以转化为 bi1b_{i-1} 的二进制是 bib_i 的子集。于是有暴力 DP:fif_i 表示考虑到第 ii 个数,找出的包含 aia_i 的合法子序列个数:

fi=j<i,ajaifjf_i = \sum_{j<i,a_j\subset a_i} f_j

复杂度 n2logan^2\log a

考虑如下两种可行的优化方法:

  1. gi=aj=ifjg_i = \sum_{a_j =i} f_j。每次增加一个数时 O(1)\mathcal O(1) 修改,转移 O(3loga)\mathcal O(3^{\log a}) 枚举子集。
  2. hi=ajifjh_i = \sum_{a_j\subset i} f_j。加数 O(3loga)\mathcal O(3^{\log a}) 枚举超集修改,转移 O(1)\mathcal O(1) 查询。

这两种单独使用复杂度都是 3loga3^{\log a},考虑优化:

  • 转移时,固定 ii 二进制后一半,前一半枚举子集。
  • 查询时,固定 ii 二进制前一半,后一半枚举超集。

然后 g,hg,h 一起用。复杂度 6loga26^{\log \frac{a}{2}}

1
2
3
4
5
6
7
8
9
10
for(register int i=1;i<=n;++i){
int l=a[i]&M,r=a[i]>>9;
for(register int j=r;j<=M;j=(j+1)|r)
g[i]=(g[i]+f[(j<<9)|l])%mod;
for(register int j=l;;j=(j-1)&l){
f[j|(r<<9)]=(f[j|(r<<9)]+g[i]+1)%mod;
if(!j) break;
}
ans=(ans+g[i])%mod;
}

转移建图

AGC005D ~K perm counting

有多少大小为 nn 的排列满足 i,piiK\forall i,\left|p_i-i\right|\neq K

n2000n\leq 2000

题解

考虑容斥,设 fif_i 为至少有 ii 个位置不符合条件的方案数。答案为:

i=0nfi(1)i\sum_{i=0}^n f_i(-1)^i

考虑到对于一个数 xx,使其不满足条件的位置不超过 22 个。将 xx 向让 xx 不合法的位置连边,就可以得到 KK 条链:

pic-2

对每条链分别 DP:fi,j,0/1,0/1f_{i,j,0/1,0/1} 表示前 ii 个点有 jj 个不符合条件,xx 有没有选,x+Kx+K 有没有选的方案数。转移是分类讨论。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int tot=0;
for(register int i=1;i<=n;++i)
if(!vis[i])
for(register int j=i;j<=n;j+=K)
vis[j]=1,a[++tot]=j;
a[0]=-(1<<30),f[0][0][0][0]=1;
for(register int i=1;i<=n;++i){
f[i][0][0][0]=1;
for(register int j=1;j<=i;++j){
bool ok1=(a[i]-a[i-1]==K),ok2=(a[i+1]-a[i]==K);
f[i][j][0][0]=(f[i-1][j][1][0]+f[i-1][j][0][0]+ok1*f[i-1][j-1][0][0])%P;
f[i][j][0][1]=ok2*(f[i-1][j-1][1][0]+f[i-1][j-1][0][0])%P;
f[i][j][1][0]=(f[i-1][j][0][1]+f[i-1][j][1][1]+ok1*f[i-1][j-1][0][1])%P;
f[i][j][1][1]=ok2*(f[i-1][j-1][0][1]+f[i-1][j-1][1][1])%P;
}
}

转化特殊情况

hiho1315 Reaching Permutations

给定一个大小为 nn 的排列。一次操作可以选出一个逆序对并交换。求经过若干次操作后可以得到多少不同的排列。

n20n\leq 20

题解

不妨考虑一下 01 序列的情况吧。

正常情况是不好考虑的,考虑 01 序列。排列 AA 能经过若干次操作到达 BB 当且仅当 i\forall iBB 中第 ii11 都不在 AA 中第 ii11 的左边。

AA 转化成 nn 个 01 序列 aia_i。其中 ai,j=[Aij]a_{i,j} = [A_i \geq j]。显然排列和 01 序列双射。而 AA 能到 BB 当且仅当 aia_i 能到 bib_i。必要性显然,充分性考虑:n1n\to 1 枚举 ii,若要把 iill 移到 rr,每次找到 (l,r](l,r] 中最大数所在的位置 xx,交换 l,xl,x

于是做一个 01 串上的 DP:从全 00 串开始,每次转移将一个 00 改成 11,到达全 11 串。复杂度 n2nn2^n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for(register int s=0;s<S;++s) if(f[s]){
int K=n-cal[s]+1,A=0;
for(register int i=0;i<n;++i)
if(a[i]>=K)
A|=(1<<i);
bool ok=1;
for(register int i=1,p1=0,p2=0;i<=cal[s];++i,++p1,++p2){
while(!(s&(1<<p1))) ++p1;
while(!(A&(1<<p2))) ++p2;
if(p1<p2){
ok=0;
break;
}
}
if(!ok) continue;
for(register int i=0;i<n;++i)
if(!(s&(1<<i)))
f[s|(1<<i)]=(f[s|(1<<i)]+f[s])%mod;
}
  • 标题: DP 入门
  • 作者: Arahc
  • 创建于 : 2022-06-21 08:00:00
  • 更新于 : 2023-03-16 13:55:34
  • 链接: https://arahc.github.io/2022/06/21/【笔记】DP-入门/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论