在这里堆一些DP题

这里放一些做不出来的DP题,方便复习总结


关于DP的随手写:
① 尽量减少有效状态数,等价的压缩在一起
② 总复杂度=状态数 * 转移复杂度
③ 划分成子问题,让每一部分可以独立计算


1、最大价值

题意:
一个序列, n n 个数 (n400) w[i] w [ i ] ,可以删除其中任意段,每次删除获得一定价值。删除的序列满足相邻两数的差为1且数值单增、单减或先增后减(不能先减后增)。已知删除每种长度的序列可获得的价值 v[len] v [ l e n ] ,求最大价值。

题解:
区间DP。感觉设的状态比较神奇,做的时候没有想出来。
f[i][j] f [ i ] [ j ] 表示将 i i j全部删除得到的最大价值, g[i][j][0/1] g [ i ] [ j ] [ 0 / 1 ] 表示保留 i i j、删除 ij i 、 j 之间若干段序列使保留部分单增/减获得的最大价值。
转移的时候, f[i][j] f [ i ] [ j ] 可以从 f[i][k]+f[k+1][j] f [ i ] [ k ] + f [ k + 1 ] [ j ] 转移过来,也可以从 g[i][k][0]+g[k][j][1]+v[2kij+1] g [ i ] [ k ] [ 0 ] + g [ k ] [ j ] [ 1 ] + v [ 2 ∗ k − i − j + 1 ] 转移过来。
最后对 f[i][j] f [ i ] [ j ] 再做一遍DP, ans[i]=max(ans[i],ans[ij]+f[ij+1][i]) a n s [ i ] = m a x ( a n s [ i ] , a n s [ i − j ] + f [ i − j + 1 ] [ i ] ) ans[n] a n s [ n ] 就是答案了。

反思:
这个题里面的 g g 数组比较神奇,强制规定了序列的开头结尾和增减性,而且利用相邻两数之差为1的性质,知道开头结尾可以很容易得到序列的长度,转移就比较方便了。
当时想设 f[i][j] 直接表示从i到j删任意段得到的最大价值,就不会转移了。所以设好状态很重要!


2、最长上升子序列计数
题意:给出一个长为m的序列 a[] a [ ] ,求有多少 n n 的排列 p[] p [ ] 满足 a[] a [ ] p[] p [ ] 的一个最长上升子序列。 n,m<=15 ( n , m <= 15 )

题解:
考虑求一个序列的 LIS 长度的经典做法,维护一个 f[] f [ ] 数组, f[i] f [ i ] 表示当前所有长度为 i i 的上升子序列中,结尾最小是多少。那么对于一个长为3的上升子序列 1 2 5,新加入 4 之后, f[i] f [ i ] 会由 5 变成 4 。
基于此,又由数据范围,可以考虑状压。压成3进制, 0 0 表示没有出现在序列里,2表示出现在序列里但不出现在 f[] f [ ] 里, 1 1 表示出现在序列里且出现在f[]里。转移的时候,枚举每一个尚未出现在当前序列中的数,将它加入,并更新 f[] f [ ]

反思:
看到这个数据范围没有想到是DP。对 LIS 的经典求法也不是很清楚,这个题就没有做出来。还是要掌握好经典模型啊。
而且,这个题在实现上也算有一些小技巧吧。
一是 1/2 的含义,二者颠倒之后在处理DP顺序的时候会比较麻烦,所以1表示出现在 f[] 中。
二是在转移的时候就保证满足序列 a[ ] 的限制,即新加入的数i==a[k]时,要判断a[1~k-1]已经在当前状态里了才能转移。这样在最后统计答案的时候只需要使 LIS 的长度等于m就可以将其计入答案了。


3、树形依赖背包
题意:经典树形依赖背包(O(nk))

解法:
dp[i][j]表示容量为j的背包,可以装dfn[x]>=i 的物品x时的最大价值。转移时,若dfn[x]=i,当j>=w[x]时有dp[i][j]=max(dp[i+siz[x]][j],dp[i+1][j-w[x]]+va[x]),前者表示不选x(同时也不选以x为根的子树中的物品),后者表示选x;当j< w[x]时有dp[i][j]=dp[i+siz[x]][j]

反思:经典模型掌握得还不够,之前只会O(nk^2)的做法。没有想到把树上的问题转化成序列上的问题。

void dfs(int now)
{
    siz[now]=1; pos[++tot]=now;
    for (int i=head[now];i;i=e[i].ne)
    {
        int v=e[i].to; dfs(v);
        siz[now]+=siz[v];
    }
}
tot=0; dfs(1); dp[n+1][0]=0;
for (int i=n;i>=1;i--)
{
    int x=pos[i];
    for (int j=0;j<=P;j++)
     if (j<w[x]) dp[i][j]=max(dp[i+siz[x]][j],0);
      else dp[i][j]=max(max(dp[i+1][j-w[x]]+va[x],dp[i+siz[x]][j]),0) ;
    }

4、BZOJ 3810 Stanovi
题意:将一个n*m的大矩形划分成若干小矩形,满足小矩形边长为正整数且至少有一侧是大矩形的边界,最小化 (k)2 ∑ ( 小 矩 形 面 积 − k ) 2

解法:关键要发现性质,一定存在一条分割线将大矩形分成两个矩形,这样就把问题转化成了若干子问题。记忆化搜索,转移的时候枚举分割线就可以了。

反思:做题的时候看到像方差的式子就开始化简,尝试化简出一些可以DP的东西,但是没有发现什么好的性质。又猜想可能是构造,也没有想出构造的方案。最终没有想到寻找切割出的图形的性质,没有发现切割线的性质。

code(贴一下记忆化搜索的代码)

LL dfs(int x,int y,int sta)  // 长,宽,是否是边界
{
    if (x>y)  // 等价的状态压在一起
    {
        swap(x,y); int tmp=(sta&3)<<2;
        tmp|=(sta&bin[3])>>3; tmp|=(sta&bin[2])>>1; sta=tmp;
        if ((sta&bin[3])&&!(sta&bin[2])) sta-=bin[3],sta|=bin[2];
        if ((sta&bin[1])&&!(sta&bin[0])) sta-=bin[1],sta|=bin[0];
    }
    int uu=(sta>>3)&1,dd=(sta>>2)&1,ll=(sta>>1)&1,rr=sta&1;
    if (~dp[x][y][sta]) return dp[x][y][sta];  // 记忆化
    if (!sta) return dp[x][y][sta]=inf;
    LL res=(LL)(x*y-k)*(x*y-k);
    if (uu+dd+ll>0&&uu+dd+rr>0)   // 枚举纵向切割线
     for (int i=1;i<x;i++)
      res=min(res,dfs(i,y,sta&(all-bin[0]))+dfs(x-i,y,sta&(all-bin[1])));
    if (uu+ll+rr>0&&dd+ll+rr>0)   // 枚举横向切割线
     for (int i=1;i<y;i++)
      res=min(res,dfs(x,i,sta&(all-bin[2]))+dfs(x,y-i,sta&(all-bin[3])));
    return dp[x][y][sta]=res;
}

5、BZOJ 1030 文本生成器
题意:求所有字符集为26、长度为m的字符串中,包含任意已知单词的字符串有多少

题解:正难则反,考虑求不包含已知单词的情况数。建出trie图DP求解即可

反思:正难则反
code(贴一下代码)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
inline int read()
{
    char c=getchar(); int num=0,f=1;
    while (c<'0'||c>'9') { if (c=='-') f=-1; c=getchar(); }
    while (c<='9'&&c>='0') { num=num*10+c-'0'; c=getchar(); }
    return num*f;
}
int n,m,ans=1,tot=1,trie[6005][26],fail[6005],f[105][6005],q[6005];
bool mark[6005];
char s[105];
void ins()
{
    int now=1,len=strlen(s+1);
    for (int i=1;i<=len;i++)
    {
        int c=s[i]-'A';
        if (!trie[now][c]) trie[now][c]=++tot;
        now=trie[now][c];
    }
    mark[now]=true;
}
void build_fail()
{
    int tou=1,wei=0;
    for (int i=0;i<26;i++)
     if (trie[1][i]) q[++wei]=trie[1][i],fail[trie[1][i]]=1;
      else trie[1][i]=1;
    while (tou<=wei)
    {
        int now=q[tou++];
        for (int i=0;i<26;i++)
         if (trie[now][i])
         {
             fail[trie[now][i]]=trie[fail[now]][i];
             q[++wei]=trie[now][i];
         }
         else trie[now][i]=trie[fail[now]][i];
        if (mark[fail[now]]) mark[now]=true;
    }
}
int main()
{
    n=read(); m=read();
    for (int i=1;i<=n;i++) scanf("%s",s+1),ins();
    build_fail(); f[0][1]=1;
    for (int i=0;i<m;i++)
     for (int now=1;now<=tot;now++)
     {
         if (mark[now]||!f[i][now]) continue;
         for (int j=0;j<26;j++)
         {
             int k=trie[now][j];
             f[i+1][k]=(f[i+1][k]+f[i][now])%10007;
         }
     }
    for (int i=1;i<=m;i++) ans=ans*26%10007;
    for (int i=1;i<=tot;i++)
     if (!mark[i]) ans=(ans-f[m][i]+10007)%10007;
    printf("%d",ans);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值