最怕你一生庸碌无为,却总是安慰自己平凡可贵。|

Mr_KaYa

园龄:2年7个月粉丝:2关注:2

AT_dp

闲话

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


AT_dp_J

  • 期望 DP

传送门

注意到 ai3,所以状态应该跟 ai 有关。考虑设 fa,b,c,d 表示有 a 种剩余 3 个的寿司,b 种剩余 2 个的寿司……

fa,b,c,d=1+anfa1,b+1,c,d+bnfa,b1,c+1,d+cnfa,b,c1,d+1+dnfa,b,c,d

整理并可得

fa,b,c,d=nnd+andfa1,b+1,c,d+bndfa,b1,c+1,d+cndfa,b,c1,d+1

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

fa,b,c=na+b+c+aa+b+cfa1,b+1,c+ba+b+cfa,b1,c+1+ca+b+cfa,b,c1

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

  • 博弈论

传送门

简单题。设 fi 表示剩余 i 个石子时当前操作者的胜负,那么显然 f0=0。考虑转移,用点博弈论的知识,必败状态后继的所有状态都是必胜状态,那么写出转移 fi=max{[fiaj=0],1jn}

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。设 fi,j 表示序列还剩下 [i,j] 时,XY 的最大值。转移要分两种情况,因为是两个人轮流取,一个会使答案增大,另一个使答案变小。

具体地,当前操作次数为偶数,先手取改变 Xfi,j=max(fi+1,j+ai,fi,j1+aj)

否则后手取,fi,j=min(fi+1,jai,fi,j1aj)

答案即为 f1,n,时间复杂度 O(n2)

与此题很类似的,还有 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]);
	}
}

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

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

形式化地,ai1,ai,ai+1(1<i<n)s.t. aiai1aiai+1,将其改为 fi=ai1+ai+1ai,剩余的不变。

这个做法代码不贴了。

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


AT_dp_M

  • 前缀和优化 DP

传送门

首先考虑一下朴素 DP,设 fi,j 表示进行到 i,已经分出去了 j 块糖的方案数。那么显然 fi,j=k=0aifi1,k。每次转移都需要扫一遍,时间复杂度 O(nk2)

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

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

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


AT_dp_N

  • 区间 DP
  • 前缀和优化 DP

传送门

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

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

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,所以设 fi,S 表示进行到第 i 行,此时选点集合为 S,复杂度 O(n22n) 会 T 掉。但观察可知已知 S 即可确定 i,是 S1 的个数,所以可以直接设为 fS,复杂度 O(n2n)

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 简单题。设 fu,0/1 表示当前点为 u,当前点是否染成黑色。那么对于 fu,0,它的子节点染成什么颜色对其没有限制,所以 fu,0=vu(fv,0+fv,1);而对于 fu,1,子节点不能还是黑色,所以 fu,1=vufv,1

最后自底向上 dfs 即可,答案为 f1,0+f1,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,设 fi 表示以 i 为结尾的最大答案,只是把每次的贡献从 1 变成 ai 了。但这样做是 O(n2) 的,考虑每次都是选择 hj<hi 的最大 fj,这样可以使用数据结构优化。fi=ai+max{fj,1j<i,hj<hi}。需要支持单点修,区间求最值,复杂度 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 是直接设 fk,i,j 表示从 ij 路径长度为 k 的方案数,然后转移

fk,i,j=u=1nfk1,i,u+f1,u,j

发现这样转移复杂度会爆炸,因为 k1018 量级的,但是这个方程你会发现跟最短路中的 Floyd 非常像,这就是个显然的矩阵乘法形式,于是想到矩阵优化,对于每一个距离矩阵 fifi=fi1×f1,那么直接快速幂即可,答案为 f1k 的所有元素和。时间复杂度 O(n3 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 发现转移数量非常多(一般达到了 109 以上的量级),可以考虑矩阵优化。


AT_dp_S

  • 数位 DP

传送门

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

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

fpos,m,limit=fpos1,(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

传送门

考虑设 fi,j 表示确定前 i 个数,其中第 i 个是 j 的方案数,那么初始状态 f1,1=1。对于转移分两种情况讨论,如果 si 是小于号,那么 fi,j=k=1j1fi1,k,否则 fi,j=k=ji1fi1,k

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

注意一下大于小于号与下标对应关系 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

传送门

看数据范围,显然是状压。考虑设 fS 表示已选兔子集合状态为 S 的答案,设 VT 表示一组中的兔子状态为 T 的贡献,那么可以写出转移方程 fS=maxTS{fT+VST}

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

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(3n),数据范围在 20 以内有限考虑状压 DP。


AT_dp_V

  • 树形 DP
  • 换根 DP

传送门

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

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

所以考虑设 fu 表示将 u 染成黑色,u 和其子树外的点所得的答案,那么显然对于每个点,最终的答案就是 fu×gu。转移方程为 fv=fu+gugv+1+1。这表示父亲 u 的答案加上 uv 的儿子的答案再加上白点的 1。时间复杂度 O(n)

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

形式化地,fv=fu+preu,i1×sufu,i+1+1。其中 i 表示 v 是第几个儿子。代码中因为前后缀积的下标从 0 开始,所以变成了 preu,i2×sufu,i,本质上没有区别。

Code:

注意开 long long 和特殊的 f1=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,把一段区间的贡献转化为右端点的贡献,排序后只有到右端点才计算贡献,用一个结构体存储区间信息。

fi,j 表示进行到位置 i,最后一个 1 在位置 j 的答案。考虑转移,如果 i=j,也即在最后一个位置放了 1,那么 fi,i=maxj=1i1fi1,j

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

fi,j=fi1,j+lkj,rk=iak

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

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

Code:

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

代码中的 vi 表示第 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 @   Mr_KaYa  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起