动态规划及其优化
dp
树形dp
顾名思义,就是树上的
例题
-
模板题,关注该节点是否能取。
void dp(int x){ f[x][0]=0,f[x][1]=h[x]; for(int i=head[x];i;i=e[i].next){ int y=e[i].to; dp(y); f[x][0]+=max(f[y][0],f[y][1]); f[x][1]+=f[y][0]; } }
-
树上背包模板题,注意背包转移时循环的方向。
void dp(int u){ f[u][1]=h[u]; for(int i=head[u];i;i=e[i].next){ int v=e[i].to; if(v==fa) continue; dp(v); for(int j=m+1;j>=1;j--){ for(int k=j-1;k>=0;k--){ f[u][j]=max(f[u][j],f[v][k]+f[u][j-k]); } } } }
-
二次扫描与换根,先一遍 dfs 求出要用的一些量,然后第二遍求出各个点作为根节点时的答案。
int n,head[maxn],idx,ans; long long dep[maxn],siz[maxn],f[maxn],x; void dfs1(int u,int fa){ siz[u]=1; dep[u]=dep[fa]+1; for(int i=head[u];i;i=e[i].next){ int v=e[i].to; if(v==fa) continue; dfs1(v,u); siz[u]+=siz[v]; } } void dfs2(int u,int fa){ for(int i=head[u];i;i=e[i].next){ int v=e[i].to; if(v==fa) continue; f[v]=f[u]+n-siz[v]*2; dfs2(v,u); } } int main(){ int u,v; scanf("%d",&n); for(int i=1;i<n;i++){ scanf("%d%d",&u,&v); add(u,v),add(v,u); } dep[0]=-1; dfs1(1,0); for(int i=1;i<=n;i++) f[1]+=dep[i]; dfs2(1,0); for(int i=1;i<=n;i++) if(x<f[i]) x=f[i],ans=i; printf("%d\n",ans); return 0; }
-
贪心也可以做,
状态设的就很神了。由于要满足 的无后效性以及正确性,第二维设 是不行的。(节点的一个儿子设立,其他儿子都可以不用设立) 表示可以覆盖到从节点 向上 层的最小消防站个数。 表示可以覆盖到从节点 向上 层的最小消防站个数。 表示可以覆盖到从节点 向上 层的最小消防站个数。 表示可以覆盖到从节点 向上 层的最小消防站个数。 表示可以覆盖到从节点 向上 层的最小消防站个数。显然,第一种初始状态应是
,其他均为 。
void dfs(int u,int fa){ int flag=0; dp[u][0]=1; dp[u][3]=dp[u][4]=0; for(int i=head[u];i;i=e[i].nxt){ int v=e[i].to; if(v==fa) continue; flag=1; dfs(v,u); dp[u][0]+=dp[v][4],dp[u][3]+=dp[v][2],dp[u][4]+=dp[v][3]; } if(!flag){ dp[u][1]=dp[u][2]=1; } else{ dp[u][1]=dp[u][2]=0x7fffffff; int res1,res2; for(int i=head[u];i;i=e[i].nxt){ int t=e[i].to; if(t==fa) continue; res1=res2=0; for(int j=head[u];j;j=e[j].nxt){ int s=e[j].to; if(s==fa) continue; if(s==t) continue; res1+=dp[s][3],res2+=dp[s][2]; } dp[u][1]=min(dp[u][1],res1+dp[t][0]); dp[u][2]=min(dp[u][2],res2+dp[t][1]); } for(int i=1;i<=4;i++) dp[u][i]=min(dp[u][i],dp[u][i-1]); } }
数位dp
对于数位上每个数的有约束的各类统计问题,可以考虑用数位 dp 解决。
通常使用记忆化递归实现(更通用),属于比较板子的 dp 了。
在进行记忆化递归时,通常需要考虑三个因素:前导零(有时需要考虑),值域边界限制(必定会有),题面要求限制。
例题
-
版子题,枚举
的数字,按位统计即可。注意前导零对答案的影响。
代码:
ll a,b,f[15][15][11][2],w[15],n; ll calc(ll pos,ll k,ll lead,ll limit,ll sum){ if(!lead&&!limit&&f[pos][sum][k][lead]!=-1) return f[pos][sum][k][lead]; if(pos>n) return sum; ll res=0,up=9; if(limit) up=w[pos]; for(int i=0;i<=up;i++){ res+=calc(pos+1,k,(lead==1&&i==0)?1:0,(limit==1&&i==up)?1:0,sum+(i==k)-(i==k&&i==0&&lead)); } if(!limit&&!lead) f[pos][sum][k][lead]=res; return res; } inline ll solve(ll k,ll now){ memset(f,-1,sizeof(f)); n=0; if(k==0) w[++n]=0; while(k){ w[++n]=k%10,k/=10; } reverse(w+1,w+1+n); return calc(1,now,1,1,0); }
-
题目的三个条件易于约束,但是求平方和难以直接转移。
考虑合并时的情况,从简单情况入手,前
位已被固定(递归时进行统计),当前第 位的数字为 ,对构成数字本身的贡献为 ,递归回来构成的数字为 。那么递归时的答案计算应是
,拆开来则是 。那么我们在递归时,就要维护三个量:可以构成的数字个数
(计算多个 ),当前对数字本身的贡献 (计算多个类似 对答案的贡献),以及当前的数字平方和 (计算多个平方对答案贡献)。前两个量合并时直接相加即可,平方和合并便是
。代码:
struct node{ ll sqsum,sum,cnt; }f[25][10][10]; ll t,l,r,a[25],n,base[25],Base[25]; node calc(ll pos,ll sum,ll now,ll limit){ if(!pos) return {0,0,(sum&&now)}; if(!limit&&f[pos][sum][now].cnt!=-1) return f[pos][sum][now]; node res={0,0,0},tmp; for(int i=0;i<=9;i++){ if(limit&&a[pos]<i) break; if(i==7) continue; tmp=calc(pos-1,(sum+i)%7,(now+i*Base[pos-1]%7)%7,limit&&a[pos]==i); res.cnt=(res.cnt+tmp.cnt)%mod; res.sum=(res.sum+tmp.sum+i*tmp.cnt%mod*base[pos-1]%mod); res.sqsum=((res.sqsum+tmp.sqsum)%mod+2*tmp.sum%mod*i%mod*base[pos-1]%mod)%mod; res.sqsum=(res.sqsum+tmp.cnt*i%mod*base[pos-1]%mod*i%mod*base[pos-1]%mod)%mod; } if(!limit) f[pos][sum][now]=res; return res; } inline ll solve(ll x){ memset(f,-1,sizeof(f)); n=0; if(x==0) a[++n]=0; while(x){ a[++n]=x%10,x/=10; } return calc(n,0,0,1).sqsum; }
-
根据题目定义,可以得到一个性质:
如果一个数是杠杆数,它的支点有且仅有一个。因为无论当前支点向左移还是向右移,左右的差必定单调递增,差必不为
。所以,只需要枚举每一个作为支点的位置,进行 dp,状态为三维:位数,支点左右差值,支点位置。最后判断差是否为
即可。
状压dp
对于一些转移或表示很麻烦的 dp,可以通过状态压缩来实现。状态压缩通过将状态的数字串作为
状态压缩其实是一种思想,不一定只局限于 dp 之中,许多题都可以用状态压缩的思想去维护或优化。状压 dp 也算是一种对 dp 的一种优化。
一般来说,状压 dp 的数据量很小,但又会比爆搜可支持的数据范围略大。
二进制状压常用位运算技巧:
- 取出数字
的第 位上的数字:x&(1<<pos)
- 判断数字
是否有相邻的 :x&(x<<1)
- 将数字
的第 位上的数字赋值为 :x|(1<<pos)
只是总结了一些常用方式,主要是要根据题目来灵活使用。
例题
-
P1879 [USACO06NOV] Corn Fields G
先将能否种草的信息用状压的方式储存,观察到数据量很小,可以状压。每次枚举本行与上一行的状态,判断是否合法(本行是否有相邻的
,本行与上一行是否有相同位置上的 )后转移即可。
for(int i=1;i<=n;i++){ for(int j=m;j>=1;j--){ scanf("%d",&x); mp[i]+=x*pow(2,j-1); } } for(int i=0;i<(1<<m);i++){ if(!(i&(i<<1))&&!(i&(i>>1))){ cnt[++idx]=i; } } dp[0][0]=1; for(int i=1;i<=n;i++){ for(int l=1;l<=idx;l++){ int j=cnt[l]; for(int r=1;r<=idx;r++){ int k=cnt[r]; if(!(j&k)&&(mp[i]&j)==j) dp[i][j]=(dp[i][j]+dp[i-1][k])%mod; } } } for(int i=1;i<=idx;i++) ans=(ans+dp[n][cnt[i]])%mod; printf("%lld\n",ans);
-
与上题类似,因为要考虑上两行的影响,维度多了上两行的状态,多枚举一维判断即可。
本题的小 trick:可以预先处理本行之内的合法状态并储存,建立状态与编号的映射。枚举时只需要遍历编号,维度的空间也只需要考虑编号的大小。省去了不必要的空间,时间也大幅优化,就不用滚动数组了。
for(int i=0;i<(1<<m);i++){//预处理 if(!(i&(i<<1))&&!(i&(i<<2))) state[++cnt]=i; } for(int i=1;i<=n;i++){ for(int j=1;j<=cnt;j++){//本行状态 if((state[j]&mp[i])!=state[j]) continue; for(int k=1;k<=cnt;k++){//上一行状态 if((state[k]&mp[i-1])!=state[k]||(state[k]&state[j])) continue; for(int las=1;las<=cnt;las++){//上上行状态 if((state[las]&mp[i-2])!=state[las]||(state[las]&state[j])||(state[las]&state[k])) continue; dp[i][j][k]=max(dp[i][j][k],dp[i-1][k][las]+count(state[j])); } } } } for(int i=1;i<=cnt;i++){ if((state[i]&mp[n])!=state[i]) continue; for(int j=1;j<=cnt;j++){ if((state[j]&mp[n-1])!=state[j]||(state[i]&state[j])) continue; ans=max(ans,dp[n][i][j]); } }
- yyy洗牌
dp优化
大多数
当然,对于空间,也有常规的滚动数组优化,以及各种小
单调队列优化
如果一个人比你小还比你强,那你就打不过他了。
关于单调队列
一种线性数据结构,可以维护固定区间长度的序列最值。
经典例题:滑动窗口 /【模板】单调队列
优化dp
满足形如
应用:单调队列优化多重背包
有一个体积为
的背包,现在有 种物品,第 个物品的体积为 ,数量为 ,价值为 。求可以获得的最大价值。
考虑将多重背包转化为
观察这个式子,我们发现
那么,我们想到根据
如何分组呢?根据我们的发现,我们可以将体积
分组后,对于每一组的体积
根据条件
暴力
二进制拆分时间复杂度:
单调队列优化多重背包时间复杂度:
代码:
for(int i=1;i<=V;i++) dp[i]=0x7fffffff; for(int i=1;i<=n;i++){ for(int d=0;d<v[i];d++){ head=1,tail=0; for(int k=0;d+k*v[i]<=V;k++){ now=d+k*v[i]; while(head<=tail&&q[tail]>=dp[now]-k*w[i]) tail--; id[++tail]=now,q[tail]=dp[now]-k*w[i]; while(head<=tail&&(now-id[head])/v[i]>c[i]) head++; dp[now]=min(dp[now],q[head]+k*w[i]); } } }
例题
-
模板题,推出
式子: 只与 有关,决策区间长度不变且单调。很明显这就是一个滑动窗口问题了,上单调队列维护最大值即可。
for(int i=1;i<=n;i++){ if(i>=l){ while(head<=tail&&dp[q[tail]]<=dp[idx]) tail--; q[++tail]=idx; while(q[head]+r<i) head++; if(dp[q[head]]!=-0x7fffffff) dp[i]=a[i]+dp[q[head]]; else dp[i]=-0x7fffffff; idx++; } else dp[i]=-0x7fffffff; }
斜率优化
本文作者:dayz_break
本文链接:https://www.cnblogs.com/dayz-break/p/18345930
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步