我与旧事归于尽,来年依旧迎花开|

dayz_break

园龄:7个月粉丝:5关注:6

2024-08-06 20:21阅读: 18评论: 1推荐: 0

动态规划及其优化

dp

树形dp

顾名思义,就是树上的 dp

例题

  1. P1352 没有上司的舞会

    模板题,关注该节点是否能取。

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];
}
}
  1. P2014 [CTSC1997] 选课

    树上背包模板题,注意背包转移时循环的方向。

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]);
}
}
}
}
  1. P3478 [POI2008] STA-Station

    二次扫描与换根,先一遍 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;
}
  1. *P2279 [HNOI2003] 消防局的设立

    贪心也可以做,dp 状态设的就很神了。由于要满足 dp 的无后效性以及正确性,第二维设 0/1 是不行的。(节点的一个儿子设立,其他儿子都可以不用设立)

    F[i][0] 表示可以覆盖到从节点 i 向上 2 层的最小消防站个数。

    F[i][1] 表示可以覆盖到从节点 i 向上 1 层的最小消防站个数。

    F[i][2] 表示可以覆盖到从节点 i 向上 0 层的最小消防站个数。

    F[i][3] 表示可以覆盖到从节点 i 向上 1 层的最小消防站个数。

    F[i][4] 表示可以覆盖到从节点 i 向上 2 层的最小消防站个数。

    显然,第一种初始状态应是 F[i][0]=1,其他均为 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 了。

在进行记忆化递归时,通常需要考虑三个因素:前导零(有时需要考虑),值域边界限制(必定会有),题面要求限制。

例题

  1. P2602 [ZJOI2010] 数字计数

    版子题,枚举 09 的数字,按位统计即可。

    注意前导零对答案的影响。

    代码:

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);
}
  1. 恨7不成妻

    题目的三个条件易于约束,但是求平方和难以直接转移。

    考虑合并时的情况,从简单情况入手,前 pos1 位已被固定(递归时进行统计),当前第 pos 位的数字为 i,对构成数字本身的贡献为 a,递归回来构成的数字为 b,c,

    那么递归时的答案计算应是 (a+b)2+(a+c)2+,拆开来则是 (a2+2ab+b2)+(a2+2ac+c2)+

    那么我们在递归时,就要维护三个量:可以构成的数字个数 k1(计算多个 a2),当前对数字本身的贡献 k2(计算多个类似 2ab 对答案的贡献),以及当前的数字平方和 k3(计算多个平方对答案贡献)。

    前两个量合并时直接相加即可,平方和合并便是 k1a2+2ak2+k3

    代码:

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;
}
  1. P1831 杠杆数

    根据题目定义,可以得到一个性质:

    如果一个数是杠杆数,它的支点有且仅有一个。因为无论当前支点向左移还是向右移,左右的差必定单调递增,差必不为 0

    所以,只需要枚举每一个作为支点的位置,进行 dp,状态为三维:位数,支点左右差值,支点位置。最后判断差是否为 0 即可。

状压dp

对于一些转移或表示很麻烦的 dp,可以通过状态压缩来实现。状态压缩通过将状态的数字串作为 n 进制的数,用十进制的形式储存下来,使得状态易于表示和转移。

状态压缩其实是一种思想,不一定只局限于 dp 之中,许多题都可以用状态压缩的思想去维护或优化。状压 dp 也算是一种对 dp 的一种优化。

一般来说,状压 dp 的数据量很小,但又会比爆搜可支持的数据范围略大。

二进制状压常用位运算技巧:

  1. 取出数字 x 的第 pos 位上的数字:x&(1<<pos)
  2. 判断数字 x 是否有相邻的 1x&(x<<1)
  3. 将数字 x 的第 pos 位上的数字赋值为 1x|(1<<pos)

只是总结了一些常用方式,主要是要根据题目来灵活使用。

例题

  1. P1879 [USACO06NOV] Corn Fields G

    先将能否种草的信息用状压的方式储存,观察到数据量很小,可以状压。每次枚举本行与上一行的状态,判断是否合法(本行是否有相邻的 1,本行与上一行是否有相同位置上的 1)后转移即可。

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);
  1. P2704 [NOI2001] 炮兵阵地

    与上题类似,因为要考虑上两行的影响,维度多了上两行的状态,多枚举一维判断即可。

    本题的小 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]);
}
}
  1. yyy洗牌

dp优化

大多数 dp 的决策并不是能在 O(1) 的时间之内解决的。根据 dp 式子的结构特征,运用一些数据结构(线段树,平衡树,单调队列等),算法(倍增,分治),还有经典的前缀和优化,斜率优化,四边形不等式优化等等。这些都可以使转移时更快地做出决策,优化时间复杂度。

当然,对于空间,也有常规的滚动数组优化,以及各种小 Trick:时间换空间,空间换时间等。优化 dp 需要我们做题时明辨特征,保持创造地去灵活变通。

单调队列优化

如果一个人比你小还比你强,那你就打不过他了。

关于单调队列

一种线性数据结构,可以维护固定区间长度的序列最值。

经典例题:滑动窗口 /【模板】单调队列

优化dp

满足形如 dp[i]=min(dp[j]+f) 的式子,当 f 不与 ij 的相关量乘积有关且满足决策单调时,可以使用单调队列优化。

应用:单调队列优化多重背包

有一个体积为 V 的背包,现在有 n 种物品,第 i 个物品的体积为 v[i],数量为 c[i],价值为 w[i]。求可以获得的最大价值。

考虑将多重背包转化为 01 背包求解时,有 dp 方程:

dp[i]=max(dp[iv[i]]+w[i],dp[i])

观察这个式子,我们发现 dp[i] 会被 dp[iv[i]] 影响,而 dp[iv[i]] 又会被 dp[i2v[i]] 影响……dp[i(c[i]1)×v[i]] 会被 dp[ic[i]×v[i]] 影响。这样的影响是跳跃着的,很难去进行直接的优化。

那么,我们想到根据 v[i] 将体积这一维分组,使得每一组之内相互影响,各个组之间互不影响。这样,我们就可以用单调队列优化了。

如何分组呢?根据我们的发现,我们可以将体积 i 除以 v[i] 的余数将 0i 的体积数分成 v[i] 组:0v[i1]。这样每一组可以满足我们的条件:每一组之内相互影响,各个组之间互不影响。

分组后,对于每一组的体积 i,ji>j),由体积 j 转移到体积 j 所需的物品个数为 (ij)/v[i]。那么有 dp 方程:

dp[i]=dp[j]+(ij)/v[i]w[i],0<ij<c[i]

根据条件 ij<c[i],很明显这可以作为一个单调队列的限制条件了。

暴力 01 背包时间复杂度:O(nVi=1nc[i])

二进制拆分时间复杂度:O(nVi=1nlog2c[i])

单调队列优化多重背包时间复杂度:O(nV)

代码:

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]);
}
}
}

例题

  1. P1725 琪露诺

    模板题,推出 dp 式子:

    dp[i]=max(dp[k]+a[i]),iLkiR

    f 只与 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 中国大陆许可协议进行许可。

posted @   dayz_break  阅读(18)  评论(1编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起