AT_dp

闲话

DP 太菜了,刷 AT 经典 DP,前面的比较简单,从 J 开始吧 qwq


AT_dp_J

  • 期望 DP

传送门

注意到 \(a_i\leq3\),所以状态应该跟 \(a_i\) 有关。考虑设 \(f_{a,b,c,d}\) 表示有 \(a\) 种剩余 \(3\) 个的寿司,\(b\) 种剩余 \(2\) 个的寿司……

\[f_{a,b,c,d}=1+\frac{a}{n}f_{a-1,b+1,c,d}+\frac{b}{n}f_{a,b-1,c+1,d}+\frac{c}{n}f_{a,b,c-1,d+1}+\frac{d}{n}f_{a,b,c,d} \]

整理并可得

\[f_{a,b,c,d}=\frac{n}{n-d}+\frac{a}{n-d}f_{a-1,b+1,c,d}+\frac{b}{n-d}f_{a,b-1,c+1,d}+\frac{c}{n-d}f_{a,b,c-1,d+1} \]

这样是四维的,发现 \(d=n-(a+b+c)\),于是变成 \(3\) 维,可以 \(O(n^3)\) 的 DP,得到最终方程

\[f_{a,b,c}=\frac{n}{a+b+c}+\frac{a}{a+b+c}f_{a-1,b+1,c}+\frac{b}{a+b+c}f_{a,b-1,c+1}+\frac{c}{a+b+c}f_{a,b,c-1} \]

Code:

cin>>n;
for(int i=1,x;i<=n;++i) cin>>x,a[x]++;
for(int i=0;i<=n;++i)
  	for(int j=0;j<=n;++j)
    	for(int k=0;k<=n;++k)
      	if(i+j+k){
        	double p=i+j+k;
        	if(i) f[i][j][k]+=(1.0*i/p*f[i-1][j+1][k]);
        	if(j) f[i][j][k]+=(1.0*j/p*f[i][j-1][k+1]);
        	if(k) f[i][j][k]+=(1.0*k/p*f[i][j][k-1]);
        	f[i][j][k]+=1.0*n/p;
      	}
cout<<fixed<<setprecision(10)<<f[a[3]][a[2]][a[1]]<<endl;

给点启示: DP 优化的一个思路:合并等价状态、消除无用状态(这个有点像百钱买百只因)。


AT_dp_K

  • 博弈论

传送门

简单题。设 \(f_i\) 表示剩余 \(i\) 个石子时当前操作者的胜负,那么显然 \(f_0=0\)。考虑转移,用点博弈论的知识,必败状态后继的所有状态都是必胜状态,那么写出转移 \(f_i=\max\{[f_{i-a_j}=0],1\le j\le n\}\)

Code:

for(int i=1;i<=k;++i){
	for(int j=1;j<=n;++j){
    	if(i-a[j]<0) continue;
    	f[i]|=(!f[i-a[j]]);
  	}
}

AT_dp_L

  • 区间 DP

传送门

一个经典的 trick。设 \(f_{i,j}\) 表示序列还剩下 \([i,j]\) 时,\(X-Y\) 的最大值。转移要分两种情况,因为是两个人轮流取,一个会使答案增大,另一个使答案变小。

具体地,当前操作次数为偶数,先手取改变 \(X\)\(f_{i,j}=\max(f_{i+1,j}+a_i,f_{i,j-1}+a_j)\)

否则后手取,\(f_{i,j}=\min(f_{i+1,j}-a_i,f_{i,j-1}-a_j)\)

答案即为 \(f_{1,n}\),时间复杂度 \(O(n^2)\)

与此题很类似的,还有 AT_tdpc_game,可以左转我的题解

Code:

for(int len=1;len<=n;++len){
	for(int i=1;i+len-1<=n;++i){
		int j=i+len-1;
		if((n-len)&1) f[i][j]=min(f[i+1][j]-a[i],f[i][j-1]-a[j]);
		else f[i][j]=max(f[i+1][j]+a[i],f[i][j-1]+a[j]);
	}
}

此题还有一个贪心的思路,可以做到线性。

考虑对于每三个数,如果中间的最大,那么一定是先手取两边,后手取中间,这样把序列中的这样结构的三个数贡献合并成两边减中间,此时序列一定先减后增,直接贪心即可。

形式化地,\(\forall a_{i-1},a_i,a_{i+1}(1<i<n),s.t.\ a_i\ge a_{i-1}\)\(a_i\ge a_{i+1}\),将其改为 \(f_i=a_{i-1}+a_{i+1}-a_i\),剩余的不变。

这个做法代码不贴了。

启示: 很多这种类似博弈两个人取数的题都可以转化成这种 DP 的状态设计,算是一个区间 DP 的套路了。


AT_dp_M

  • 前缀和优化 DP

传送门

首先考虑一下朴素 DP,设 \(f_{i,j}\) 表示进行到 \(i\),已经分出去了 \(j\) 块糖的方案数。那么显然 \(f_{i,j}=\sum_{k=0}^{a_i}f_{i-1,k}\)。每次转移都需要扫一遍,时间复杂度 \(O(nk^2)\)

但是发现每一层 DP 的状态只与上一层有关,所以可以直接使用前缀和优化,时间复杂度 \(O(nk)\)

代码不贴了,注意数组下标,前缀和容易 RE。

启示: 当发现 DP 转移时方程只与上一层有关时,可以考虑用一些求和数据结构优化(比如前缀和)。


AT_dp_N

  • 区间 DP
  • 前缀和优化 DP

传送门

区间 DP 傻题,和 石子合并 一模一样。

需要注意的一点是,朴素直接计算贡献是 \(O(n^4)\) 的,使用前缀和优化到 \(O(n^3)\)

Code:

for(int i=n;i>=1;--i)
    for(int j=i+1;j<=n;++j)
      	for(int k=i;k<j;++k)
      		f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);

AT_dp_O

  • 状压 DP

传送门

拜谢老头。 看数据范围考虑状压 DP。发现一个合法的方案一定满足每一行、每一列只选一个 \(1\),所以设 \(f_{i,S}\) 表示进行到第 \(i\) 行,此时选点集合为 \(S\),复杂度 \(O(n^22^n)\) 会 T 掉。但观察可知已知 \(S\) 即可确定 \(i\),是 \(S\)\(1\) 的个数,所以可以直接设为 \(f_S\),复杂度 \(O(n2^n)\)

Tips:popcount 用来计算二进制 \(1\) 的个数。

Code:

f[0]=1;
for(int S=0;S<(1<<n);++S){
	int k=__builtin_popcount(S);
	for(int j=0;j<n;++j){
		if(!(S&(1<<j))&&a[k][j])
			f[S|(1<<j)]=(f[S|(1<<j)]+f[S])%mod;
	}
}

AT_dp_p

  • 树形 DP

传送门

没有舞会的上司。 树上 DP 简单题。设 \(f_{u,0/1}\) 表示当前点为 \(u\),当前点是否染成黑色。那么对于 \(f_{u,0}\),它的子节点染成什么颜色对其没有限制,所以 \(f_{u,0}=\prod_{v\in u}(f_{v,0}+f_{v,1})\);而对于 \(f_{u,1}\),子节点不能还是黑色,所以 \(f_{u,1}=\prod_{v\in u}f_{v,1}\)

最后自底向上 dfs 即可,答案为 \(f_{1,0}+f_{1,1}\),线性复杂度。

Code:

void dfs(int u,int fa){
	f[u][0]=f[u][1]=1;
	for(int v:e[u]){
		if(v==fa) continue;
		dfs(v,u);
		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

  • 数据结构优化 DP

传送门

最长上升子序列加权版。

发现朴素的 DP 就是 朴素的 LIS,设 \(f_i\) 表示以 \(i\) 为结尾的最大答案,只是把每次的贡献从 \(1\) 变成 \(a_i\) 了。但这样做是 \(O(n^2)\) 的,考虑每次都是选择 \(h_j<h_i\) 的最大 \(f_j\),这样可以使用数据结构优化。\(f_i=a_i+\max\{f_j,1\le j<i,h_j<h_i\}\)。需要支持单点修,区间求最值,复杂度 \(O(n\ logn)\)

这里使用了树状数组,因为要求的是前缀最大值,而且单点修改,树状数组比线段树更容易实现。

Code:

树状数组部分

void add(int x,ll k){
	for(int i=x;i<=n;i+=lowbit(i)) c[i]=max(c[i],k);
}
ll query(int x){
	ll res=0;
	for(int i=x;i;i-=lowbit(i)) res=max(res,c[i]);
	return res;
}

主函数部分

for(int i=1;i<=n;++i){
	ll x,a;
	cin>>a;
	x=query(h[i]-1)+a;
	ans=max(ans,x);
	add(h[i],x);
}

启示: 对于一个 DP 转移方程,如果转移复杂度很高,要求的东西只与之前的值有关,可以尝试数据结构优化。


AT_dp_R

  • 矩阵加速 DP

传送门

题目要求路径长度为 \(k\) 的路径数,朴素的 DP 是直接设 \(f_{k,i,j}\) 表示从 \(i\)\(j\) 路径长度为 \(k\) 的方案数,然后转移

\[f_{k,i,j}=\sum_{u=1}^nf_{k-1,i,u}+f_{1,u,j} \]

发现这样转移复杂度会爆炸,因为 \(k\)\(10^{18}\) 量级的,但是这个方程你会发现跟最短路中的 Floyd 非常像,这就是个显然的矩阵乘法形式,于是想到矩阵优化,对于每一个距离矩阵 \(f_i\)\(f_i=f_{i-1}\times f_1\),那么直接快速幂即可,答案为 \(f_1^{k}\) 的所有元素和。时间复杂度 \(O(n^3\ logk)\),可以通过。

Code:

struct matrix{
	ll a[N][N];
	matrix(){memset(a,0,sizeof(a));}
}a;
matrix operator * (const matrix&a,const matrix&b){
	matrix res;
	for(int i=0;i<n;++i)
		for(int j=0;j<n;++j)
			for(int k=0;k<n;++k)
				res.a[i][j]=(res.a[i][j]+a.a[i][k]*b.a[k][j]%mod)%mod;
	return res;
}
matrix ksm(matrix a,ll k){
	matrix res;
	for(int i=0;i<n;++i) res.a[i][i]=1;
	while(k){
		if(k&1) res=res*a;
		a=a*a;
		k>>=1;
	}
	return res;
}

启示: 当一个线性 DP 发现转移数量非常多(一般达到了 \(10^9\) 以上的量级),可以考虑矩阵优化。


AT_dp_S

  • 数位 DP

传送门

看数据范围超级大想数位 DP。考虑记录 \(3\) 个状态:当前位、是否达到最高位限制、当前数各位和模 \(D\) 结果,边界条件是最后模 \(D\)\(0\) 了答案加一。

具体地,设 \(f_{pos,m,limit}\) 表示当前考虑到第 \(pos\) 位(倒序从最高位往后找),各个位上的和模 \(D\)\(m\),是否顶到上界。边界条件 \(f_{0,0,0/1}=1\),转移时

\[f_{pos,m,limit}=\sum f_{pos-1,(m+i)\%D,limit\&\&[i=n]} \]

注意,\(i\) 表示当前枚举的下一位,\(n\) 表示下一位的最大值。

警钟: 最后的答案要 -1,因为 \(0\) 不算,但减一以后可能会变成负数(全为 \(0\) 的情况),所以还要加 mod 再对 mod 取模。

代码用记忆化搜索实现(因为太菜了不会递推),其实 \(f\) 的第三维 \(limit\) 可以不用设,但为了清晰还是写了。

Code:

记忆化搜索部分

int dfs(int pos,int m,int limit){
	if(pos==0) return (m==0);
	if(~f[pos][m][limit]) return f[pos][m][limit];
	int n=limit?a[pos]:9,res=0;
	for(int i=0;i<=n;++i) res=(res+dfs(pos-1,(m+i)%d,limit&&(i==n)))%mod;
	return f[pos][m][limit]=res;
}

主函数部分

int l=strlen(s);
for(int i=0;i<l;++i) a[l-i]=s[i]-'0';
memset(f,-1,sizeof(f));
cout<<(dfs(l,0,1)-1+mod)%mod;

启示: 当计算某些数的数量,且范围很大,考虑数位 DP,记忆化搜索可以便捷实现,注意是否要考虑前导零。


AT_dp_T

  • 前缀和优化 DP

传送门

考虑设 \(f_{i,j}\) 表示确定前 \(i\) 个数,其中第 \(i\) 个是 \(j\) 的方案数,那么初始状态 \(f_{1,1}=1\)。对于转移分两种情况讨论,如果 \(s_i\) 是小于号,那么 \(f_{i,j}=\sum_{k=1}^{j-1}f_{i-1,k}\),否则 \(f_{i,j}=\sum_{k=j}^{i-1}f_{i-1,k}\)

朴素的转移是 \(O(n^3)\) 的。发现又是只与上一层状态有关,可以通过前缀和优化为 \(O(n^2)\),于是可以通过此题。

注意一下大于小于号与下标对应关系 qwq。

Code:

f[1][1]=s[1][1]=1;
for(int i=2;i<=n;++i){
	for(int j=1;j<=i;++j){
		if(ch[i]=='<') f[i][j]=s[i-1][j-1]%mod;
		else f[i][j]=(f[i][j]+s[i-1][i-1]-s[i-1][j-1]+mod)%mod;
		s[i][j]=(s[i][j-1]+f[i][j])%mod;
	}
}
for(int i=1;i<=n;++i) ans=(ans+f[n][i])%mod;

启示:\(M\) 题相同。


AT_dp_U

  • 状压 DP

传送门

看数据范围,显然是状压。考虑设 \(f_S\) 表示已选兔子集合状态为 \(S\) 的答案,设 \(V_T\) 表示一组中的兔子状态为 \(T\) 的贡献,那么可以写出转移方程 \(f_S=\max_{T\in S}\{f_T+V_{\complement_ST}\}\)

那其实就做完了,先枚举集合并预处理数组 \(V\),复杂度 \(O(2^nn^2)\),然后枚举子集转移,复杂度 \(O(3^n)\),于是做完了。

Code:

for(int s=1;s<(1<<n);++s){
    for(int i=0;i<n;++i){
        for(int j=i+1;j<n;++j){
            if(((s>>i)&1)&&((s>>j)&1)){
                v[s]+=a[i][j];
            }
        }
    }
    f[s]=v[s];
}
for(int s=1;s<(1<<n);++s){
    for(int cs=s;cs>0;cs=(cs-1)&s){
        f[s]=max(f[s],f[cs]+v[s^cs]);
    }
}

启示: 枚举子集复杂度 \(O(3^n)\),数据范围在 \(20\) 以内有限考虑状压 DP。


AT_dp_V

  • 树形 DP
  • 换根 DP

传送门

考虑树形 DP。设 \(g_u\) 表示 \(u\) 染成黑点,\(u\) 的子树内的答案,转移的话可以 \(g_u=\prod_{v\in u}(g_v+1)\),表示对于每个儿子 \(v\),都计算 \(v\) 子树内的答案 \(g_v\),以及 \(v\) 染成白色,其子树全是白色的 \(1\)。这样总体做下来一次是 \(O(n)\) 的。

但是题目要求输出每一个点为根的答案,一次 dfs 只能计算一个点为根的答案,总体复杂度 \(O(n^2)\) 不能接受,所以考虑使用换根 DP,在第二次 dfs 中求出所有点的答案。

所以考虑设 \(f_u\) 表示将 \(u\) 染成黑色,\(u\) 和其子树外的点所得的答案,那么显然对于每个点,最终的答案就是 \(f_u\times g_u\)。转移方程为 \(f_v=f_{u}+\frac{g_{u}}{g_v+1}+1\)。这表示父亲 \(u\) 的答案加上 \(u\)\(v\) 的儿子的答案再加上白点的 \(1\)。时间复杂度 \(O(n)\)

本来这个题已经快乐地做完了,但问题在于答案需要取模,模数不一定是质数。这就十分棘手,因为你无法直接算逆元。所以考虑对于每个点 \(u\) 记一个前缀积 \(pre\) 和后缀积 \(suf\),在第一遍 dfs 中预处理。这样转移时除法就变成了挖去此点的前后缀积。

形式化地,\(f_v=f_u+pre_{u,i-1}\times suf_{u,i+1}+1\)。其中 \(i\) 表示 \(v\) 是第几个儿子。代码中因为前后缀积的下标从 \(0\) 开始,所以变成了 \(pre_{u,i-2}\times suf_{u,i}\),本质上没有区别。

Code:

注意开 long long 和特殊的 \(f_1=1\)

void dfs1(int u,int fa){
	g[u]=1;
	for(int v:e[u]){
		if(v==fa) continue;
		dfs1(v,u);
		g[u]=g[u]*(g[v]+1)%mod;
		pre[u].push_back(g[v]+1);
		suf[u].push_back(g[v]+1);
	}
	int l=pre[u].size();
	for(int i=1;i<l;++i) pre[u][i]=1ll*pre[u][i-1]*pre[u][i]%mod;
	for(int i=l-2;i>=0;--i) suf[u][i]=1ll*suf[u][i+1]*suf[u][i]%mod;
}
void dfs2(int u,int fa){
	int l=pre[u].size(),cnt=0;
	for(int v:e[u]){
		if(v==fa) continue;
		cnt++;
		if(l==1) f[v]=f[u]+1;
		else if(cnt==1) f[v]=f[u]*suf[u][1]%mod+1;
		else if(cnt==l) f[v]=f[u]*pre[u][cnt-2]%mod+1;
		else f[v]=f[u]*pre[u][cnt-2]%mod*suf[u][cnt]%mod+1;
		dfs2(v,u);
	}
}

启示: 换根 DP 的套路。

\(1.\) 指定某个节点为根节点(一般为 \(1\))。

\(2.\) 第一次搜索完成预处理,同时得到该节点的解。

\(3.\) 第二次搜索进行换根 DP,由已知节点推出相邻节点。


AT_dp_W

  • 数据结构优化 DP

传送门

跟 NOIP T4 有点像对吧。先用一个经典的 trick,把一段区间的贡献转化为右端点的贡献,排序后只有到右端点才计算贡献,用一个结构体存储区间信息。

\(f_{i,j}\) 表示进行到位置 \(i\),最后一个 \(1\) 在位置 \(j\) 的答案。考虑转移,如果 \(i=j\),也即在最后一个位置放了 \(1\),那么 \(f_{i,i}=\max_{j=1}^{i-1}f_{i-1,j}\)

否则 \(i\neq j\),考虑每一个转移相对于 \(i-1\) 都是加上了右端点在 \(i\) 且左端点包含 \(j\) 的答案。

\[f_{i,j}=f_{i-1,j}+\sum_{l_k\le j,r_k=i} a_k \]

这样朴素转移复杂度是 \(O(n^2)\) 的,必须优化。考虑当 \(i=j\),查询了最值,对于 \(i\neq j\),所有右端点 \(i\) 的区间贡献都要累加到 DP 数组里,这是区间加的操作。于是可以使用线段树维护 DP 数组优化一下转移,这样的复杂度变成了 \(O(n\ log n)\)

空间复杂度部分,可以直接压到一维。注意每次的答案要与 \(0\)\(\max\)

Code:

线段树部分就不放了,纯板子,查询是查询整体最值。

代码中的 \(v_i\) 表示第 \(i\) 个命令。

for(int i=1;i<=n;++i){
	update(1,i,i,query());
	for(auto u:v[i]) update(1,u.l,i,u.w);
}
posted @ 2024-03-25 10:35  Mr_KaYa  阅读(32)  评论(0)    收藏  举报