这里放一些做不出来的DP题,方便复习总结
关于DP的随手写:
① 尽量减少有效状态数,等价的压缩在一起
② 总复杂度=状态数 * 转移复杂度
③ 划分成子问题,让每一部分可以独立计算
1、最大价值
题意:
一个序列,
n
n
个数
w[i]
w
[
i
]
,可以删除其中任意段,每次删除获得一定价值。删除的序列满足相邻两数的差为1且数值单增、单减或先增后减(不能先减后增)。已知删除每种长度的序列可获得的价值
v[len]
v
[
l
e
n
]
,求最大价值。
题解:
区间DP。感觉设的状态比较神奇,做的时候没有想出来。
设
f[i][j]
f
[
i
]
[
j
]
表示将
i
i
到全部删除得到的最大价值,
g[i][j][0/1]
g
[
i
]
[
j
]
[
0
/
1
]
表示保留
i
i
和、删除
i、j
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[2∗k−i−j+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[i−j]+f[i−j+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、最长上升子序列计数
题意:给出一个长为的序列
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
表示没有出现在序列里,表示出现在序列里但不出现在
f[]
f
[
]
里,
1
1
表示出现在序列里且出现在里。转移的时候,枚举每一个尚未出现在当前序列中的数,将它加入,并更新
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;
}