AT_dp
闲话
DP 太菜了,刷 AT 经典 DP,前面的比较简单,从 J 开始吧 qwq
AT_dp_J
- 期望 DP
注意到
整理并可得
这样是四维的,发现
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
- 博弈论
简单题。设
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。设
具体地,当前操作次数为偶数,先手取改变
否则后手取,
答案即为
与此题很类似的,还有 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]);
}
}
此题还有一个贪心的思路,可以做到线性。
考虑对于每三个数,如果中间的最大,那么一定是先手取两边,后手取中间,这样把序列中的这样结构的三个数贡献合并成两边减中间,此时序列一定先减后增,直接贪心即可。
形式化地,
这个做法代码不贴了。
启示: 很多这种类似博弈两个人取数的题都可以转化成这种 DP 的状态设计,算是一个区间 DP 的套路了。
AT_dp_M
- 前缀和优化 DP
首先考虑一下朴素 DP,设
但是发现每一层 DP 的状态只与上一层有关,所以可以直接使用前缀和优化,时间复杂度
代码不贴了,注意数组下标,前缀和容易 RE。
启示: 当发现 DP 转移时方程只与上一层有关时,可以考虑用一些求和数据结构优化(比如前缀和)。
AT_dp_N
- 区间 DP
- 前缀和优化 DP
区间 DP 傻题,和 石子合并 一模一样。
需要注意的一点是,朴素直接计算贡献是
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。发现一个合法的方案一定满足每一行、每一列只选一个
Tips:popcount 用来计算二进制
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 简单题。设
最后自底向上 dfs 即可,答案为
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,设
这里使用了树状数组,因为要求的是前缀最大值,而且单点修改,树状数组比线段树更容易实现。
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
题目要求路径长度为
发现这样转移复杂度会爆炸,因为
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 发现转移数量非常多(一般达到了
AT_dp_S
- 数位 DP
看数据范围超级大想数位 DP。考虑记录
具体地,设
注意,
警钟: 最后的答案要 -1,因为
代码用记忆化搜索实现(因为太菜了不会递推),其实
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
考虑设
朴素的转移是
注意一下大于小于号与下标对应关系 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;
启示: 与
AT_dp_U
- 状压 DP
看数据范围,显然是状压。考虑设
那其实就做完了,先枚举集合并预处理数组
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]);
}
}
启示: 枚举子集复杂度
AT_dp_V
- 树形 DP
- 换根 DP
考虑树形 DP。设
但是题目要求输出每一个点为根的答案,一次 dfs 只能计算一个点为根的答案,总体复杂度
所以考虑设
本来这个题已经快乐地做完了,但问题在于答案需要取模,模数不一定是质数。这就十分棘手,因为你无法直接算逆元。所以考虑对于每个点
形式化地,
Code:
注意开 long long 和特殊的
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 的套路。
指定某个节点为根节点(一般为 )。
第一次搜索完成预处理,同时得到该节点的解。
第二次搜索进行换根 DP,由已知节点推出相邻节点。
AT_dp_W
- 数据结构优化 DP
跟 NOIP T4 有点像对吧。先用一个经典的 trick,把一段区间的贡献转化为右端点的贡献,排序后只有到右端点才计算贡献,用一个结构体存储区间信息。
设
否则
这样朴素转移复杂度是
空间复杂度部分,可以直接压到一维。注意每次的答案要与
Code:
线段树部分就不放了,纯板子,查询是查询整体最值。
代码中的
for(int i=1;i<=n;++i){
update(1,i,i,query());
for(auto u:v[i]) update(1,u.l,i,u.w);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探