动态规划基础
本节动态规划(Dynamic Programming,DP),分析使用的是闫氏DP分析法。
DP分析法:从集合角度分析
DP问题优化
对方程进行等价变换,先思考朴素方法再优化。
以01背包为例,具体模型下一节,
状态表示:化零为整
f(i,j)存的是集合,的属性值,f(i,j)表示的含义是集合--->满足条件的所有选法的集合,的属性--->集合中只从前i个物品里面选且总体积小于等于j时,的最大值
答案:假设物品总量是N,背包容量V,答案就是f(N,V)
状态计算:化整为零
集合划分,把当前集合f(i,j)划分成若干个子集,同时每个子集都能算出来
(划分原则:不重不漏,一定需要不漏,但有时可以重复,如求最大值)
--->(以01背包为例)
分成两个集合:包含i与不包含i
不包含i:即从1~i-1中选,且总体积不超过j,的集合,即f(i-1,j)
包含i:即从1~i中选,包含i,且总体积不超过j,的集合,
即选i,占用一个v[i],(由于前提是包含i,这部分已经固定);再从1~i-1中选,且这些总体积不超过j-v[i]的集合,
即f(i-1,j-v[i])+w[i]
--->故
时间复杂度:状态数量*转移数量
输出具体一步一步怎么过来的:单独开一个数组记录当前坐标从哪一个状态坐标转移过来,若没有上一个状态/现在状态是起点,就设置为-1或者一个比较特殊的数,这个数字同时可以作为回溯的终点,最后再输出
背包问题
01背包
模型:在一个背包容积固定的情况下,能装下的最大价值是多少(不要求装满背包)?假设有若干物品(每件物品只有一个),每种物品体积以及价值不同。
按照DP分析法的思路
朴素的二维写法
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
//从1开始,下标具有实际含义
//初始化f[0][0~m]全是0,全局变量默认0,略去
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
{
f[i][j]=f[i-1][j];
if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
//只有j>=v[i]的前提下,子集情况“必包含i”才有意义
}
cout<<f[n][m]<<endl;
return 0;
}
优化成一维
/*
算f[i][j]只用到了f[i-1]这一层的[j]与[j-v[i]],即0~i-2都没有用,同时j与j-v[i]都在j的一侧,都小于等于j,所以可以用滚动数组,改成一维数组计算,不断用老一层(i-1)数据来计算新一层(i)数据再覆盖掉老一层数据
f[i][j]=f[i-1][j]删去第一维度之后是f[j]=f[j],直接略去;
由于if(j>=v[i]),循环只有在j=v[i]~m之间才有效:改循环上下限
在循环是for(int j=v[i];j<=m;j++)情况下
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);直接删去第一维变成
f[j]=max(f[j],f[j-v[i]]+w[i]),然而这时f[j-v[i]]等价的是f[i][j-v[i]]而不是f[i-1][j-v[i]],
故让j从小到大更新显然不行,
此时解决方法是让j倒着算for(int j=m;j>=v[i];j--)
这样根据j每次更新f[j]的时候,f[j-v[i]] (后更新数据) 相对于f[j] (先更新) 用的就是老一层(i-1)的数据
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
//相对二维改动部分:
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m]<<endl;
return 0;
}
完全背包
模型:相对于01背包而言,每件物品有无限个
分析:
状态计算:子集为第i个物品选几次,从不含到含1个、含2个...含k个,k的范围是k*v[i]<=j,即不超过容量j
朴素代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N],w[N],v[N];
int n,m;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;v[i]*k<=j;k++)
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
cout<<f[n][m];
return 0;
}
优化
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N][N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
{
f[i][j]=f[i-1][j];
if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}//注意这里二维优化为一维的思想,用[j-v]与[j]来更新[j],在更新操作时,由于j++,j是递增的,[j-v]已经更新,是第i层的,而[j]正要更新,所以仍然是第i-1层的数据,更新之后j是第i层的,即f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])可以直接化为f[j]=max(f[j],f[j-v[i]]+w[i]),j++.(注意代码里的f[i][j]在if之前实际是f[i-1][j],先被赋值的)
cout<<f[n][m]<<endl;
return 0;
}
进一步优化为一维
//优化原理见上一个代码块注释
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
int v[N],w[N];
int f[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=v[i];j<=m;j++)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout<<f[m]<<endl;
return 0;
}
多重背包
模型:相对于01背包而言,每件物品有S[i]个
分析:
状态计算:子集为第i个物品选几个,从0个到k个,k要同时小于,第i个物品最大数量,&&k*v[i]<=j
朴素写法:也就比完全背包朴素写法第三层循环多一个条件k<=s[i]
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int f[N][N],w[N],v[N],s[N];
int n,m;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;v[i]*k<=j&&k<=s[i];k++)
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
cout<<f[n][m];
return 0;
}
优化
按照完全背包的思路不可行
多重背包的二进制优化方法
假设说s[i]=1023,将物品打包,每个打包只有一份/只能选一次,分别将1,2,4,...,512个物品打包,每包视为01背包中的一项,这些数字一定能表示出0~1023中任意一个数字,因为他们的二进制码分别是1,10,100,...1000000000.
进一步,假设s[i]=200,那么只要1,2,4,8,16,32,64,73即可,验证:1可表示01,所以1,2可表示03任一个数,以此类推1,2,4,8,16,32,64可表示0127任一个数,加上73边可表示0200任一个数。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12010,M = 2010;//N=1000*log2(2000),每个物品假设有k个,最多能拆成log2(k)个
int n,m;
int v[N],w[N];
int f[N];
int main()
{
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++)//根据物品件数拆分每个物品
{
int a,b,s;
cin>>a>>b>>s;
int k=1;
while(k<=s)
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0)
{
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
n=cnt;
//做一遍01背包
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m]<<endl;
return 0;
}
分组背包
模型:相对于01背包而言,物品会被分类,每类物品只能拿一个,同类物品不能拿第二个。
分析:
状态计算:不选第i组即f[i-1,j],或选第i组的第几个物品f[i-1,j-v[i,k]]+w[i,k]
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n,m;
int f[N],s[N],v[N][N],w[N][N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>s[i];
for (int j=0;j<s[i];j++)
cin>>v[i][j]>>w[i][j];
}
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--)
//若写二维f[i][j]=f[i-1][j]在这,不要放到第三层循环里了
for(int k=0;k<s[i];k++)
if(v[i][k]<=j)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
cout<<f[m];
return 0;
}
线性DP
例.AcWing898 数字三角形
分析
状态表示需要两维坐标,(i,j)即表示第i行第j个点
状态计算:f(i,j)分为两类,来自左上方f(i-1,j-1),来自右上方f(i-1,j),取max最后统一加上(i,j)的值
题解
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510,INF = 1e9;
int a[N][N],f[N][N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin>>a[i][j];
for(int i=0;i<=n;i++)
for(int j=0;j<=n+1;j++)
f[i][j]=-INF;
f[1][1]=a[1][1];
for(int i=2;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
int ans=-INF;
for(int i=1;i<=n;i++) ans=max(ans ,f[n][i]);
cout<<ans;
return 0;
}
例.AcWing895、896 最长上升子序列
注意:子序列并不要求连续
分析
状态表示需要一维坐标,i
状态计算:
对于f(i),一定选i,所以考虑i-1选的情况,首先不选前一项,从f[i]=1开始,其次i-1选从以第1、2...i-1位为结尾(不一定每一位都满足条件),f[i]=max+1
示例( O(n2) ):
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N],a[N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)
{
f[i]=1;
for(int j=1;j<i;j++)
if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
}
int res=-1;
for(int i=1;i<=n;i++) res=max(res,f[i]);
cout<<res;
return 0;
}
要输出具体是哪一串子序列:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N], a[N];
int g[N];
void show(int k)
{
if (k == 0) return;
show(g[k]);
cout << a[k] << ' ';
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++)
{
f[i] = 1;
g[i] = 0;
for (int j = 1; j < i; j++)
if (a[i] > a[j])
if (f[i] < f[j] + 1)
{
f[i] = f[j] + 1;
g[i] = j;//记录每个状态是从哪个状态转移过来的
}
}
int k = 1;//记录最优解下标
for (int i = 1; i <= n; i++)
if (f[i] > f[k])
k = i;
cout << f[k] << endl;
show(k);
return 0;
}
优化( O(nlogn) ):当数据量很大时
假如现在在求f[i],在i之前,长度相同的子序列,只要留下结尾最小的就行了,因为比它大的一定不如小的好。
性质:不同长度子序列结尾值依长度递增,反证易得。
思路:在算i时,用二分找到最大长度满足结尾值小于第i位的数字,然后再看此时的i是否比最大长度+1的子序列末尾旧值小(其实一定是小于或者等于,因为满足条件情况下找最大,也就是从小到大找第一个不满足条件的前一个),若小,就更新
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int a[N],q[N];//q存(在i之前)不同长度子序列最小末尾值
int main()
{
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
int len=0;//目前最长子序列长度
for(int i=0;i<n;i++)//遍历a[i],q数组的下标就是子序列长度
{
int l=0,r=len;//这个l,r是用于二分查找q数组的,q数组有l与r的讲法
while(l<r)
{
int mid=l+r+1>>1;
if(q[mid]<a[i]) l=mid;//check()描述为q[mid]<a[i],找到最大的满足小于a[i]的长度数组下标
//由于是在长为r后接a[i]使其变为r+1,为保证严格单调递增,必须<而不能<=
else r=mid-1;
}
len = max(len,r+1);//取max,若r+1小于len,因为q数组下标表示r表示现在i能接上的最长的子序列,r+1也就是0~i的最长子序列。
q[r+1]=a[i];//q[r]是q中最后一个小于a[i]的,q[r+1]必然大于等于a[i]
}
cout<<len;
return 0;
}
例.AcWing897 最长公共子序列
分析
状态表示需要两维坐标,(i,j)
状态计算:四个子集(这四个子集的“表达”有重叠部分,他们的表达并不完全一一对应子集,只要不漏就行):
不选a[i]不选b[j],即f(i-1,j-1);
不选a[i]选b[j],不是f(i-1,j),因为这个集合长度最大的选择不一定以j结尾,但我一定要选b[j],但又由于f(i-1,j)这个集合肯定包含 选a[i]不选b[j] 这种情况,只是属性最大值并不一定来自于它,而f(i,j)肯定包含f(i-1,j),也就是说f(i-1,j)包含的集合与其他三种情况有重叠,同时又一定包含现在的选法,并且由于我是在求“最大值”,所以可以用f(i-1,j)来代表这一个选项;
选a[i]不选b[j],f(i,j-1),解释与第二种情况同理;
选a[i]选b[j],即f(i-1,j-1)+1
示例
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
char a[N],b[N];
int f[N][N];
int main()
{
int n,m;
cin>>n>>m>>a+1>>b+1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
f[i][j]=max(f[i-1][j],f[i][j-1]);//这里略去f[i-1][j-1],因为肯定都被f[i-1][j]与f[i][j-1]包含了
if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
}
cout<<f[n][m];
return 0;
}
例.AcWing902 最短编辑距离
分析
状态表示需要两维坐标,[i,j]第i堆石子到第j堆石子区间
状态计算 ( O(n2) ):
三个子集,对于a[i]:增/删/改
若删 说明a[i]和b[j]不同,但和a[i-1]相同,则f(i-1,j)+1
若增加 添在a[i]后添加b[j],即a和b的前j-1匹配,即f(i,j-1)+1
若改 说明要做的操作是先让a[i-1]与b[j-1]以及之前全都相同,即f(i-1,j-1)步,如果a[i]和b[j]不相等要改,则+1,如果本身就相同,则+0不需要改。
题解
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
char a[N],b[N];
int f[N][N];
int main()
{
cin>>n>>a+1;
cin>>m>>b+1;
//初始化
for(int i=0;i<=m;i++) f[0][i]=i;//第一个长0,那第二个有多长,增多长
for(int i=0;i<=n;i++) f[i][0]=i;//第二个长0,那第一个有多长,减多长
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);
if(a[i]==b[j]) f[i][j]=min(f[i][j],f[i-1][j-1]);
else f[i][j]=min(f[i][j],f[i-1][j-1]+1);
}
cout<<f[n][m];
return 0;
}
例.AcWing899 编辑距离
也就是上道例题的改编,计算时间复杂度后发现可以用上面的方法。
题解
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 15,M = 1010;
int n,m;
int f[N][N];
char str[M][N];
int edit_distance(char a[],char b[])
{
int alen=strlen(a+1),blen=strlen(b+1);
//int alen=strlen(a)-1,blen=strlen(b)-1;是错的,在C++中char在全局变量中初始化是0,就是休止符号,根据strlen的原理是找休止符号位置,所以strlen(a)一上来就等于0
for(int i=0;i<=blen;i++) f[0][i]=i;
for(int i=0;i<=alen;i++) f[i][0]=i;
for(int i=1;i<=alen;i++)
for(int j=1;j<=blen;j++)
{
f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);
if(a[i]==b[j]) f[i][j]=min(f[i][j],f[i-1][j-1]);
else f[i][j]=min(f[i][j],f[i-1][j-1]+1);
}
return f[alen][blen];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%s",str[i]+1);
while(m--)
{
char s[N];
int limit;
scanf("%s%d",s+1,&limit);
int res=0;
for(int i=0;i<n;i++)
{
int len=edit_distance(str[i],s);
if(len<=limit) res++;
}
printf("%d\n",res);
}
return 0;
}
区间DP
例.AcWing282 石子合并
分析
状态表示需要两维坐标,[i,j]第i堆石子到第j堆石子区间
状态计算 ( O(n3) ):
以合并的倒数第二步还剩两堆时的分界线为划分子集依据:即一开始左边1、2、3...j-i个,下表为i~j-1,代价即 f(i,k)+f(k+1,j)+最后一步代价 的最小值 最后一步代价最小值不用从i加到j,利用前缀和快速计算s[j]-s[i-1]
为了保证每个子集都已经计算过,按照区间长度从小到大的顺序来枚举
题解
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n;
int s[N];
int f[N][N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&s[i]);
for(int i=1;i<=n;i++) s[i]=s[i-1]+s[i];//前缀和
for(int len=2;len<=n;len++)//枚举长度
for(int i=1;i+len-1<=n;i++)//枚举起点
{
int l=i,r=l+len-1;
f[l][r]=0x3f3f3f3f;
for(int k=l;k<r;k++)//把k作为分界点,其中k属于左侧,f[k+1]要合法,所以k+1<=r,即k<r
{
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
}
}
printf("%d",f[1][n]);
return 0;
}
计数类DP
例.AcWing900 整数划分
方案一:当作被背包问题,思路,将背包容量视为n,然后分别有1,2,3...n种体积与价值的物品,每种物品无限个,看作完全背包,找出恰好填满背包的方案数。
分析:
状态计算:子集为第i个物品选几次,从不含到含1个、含2个...含k个,k的范围是k*v[i]<=j,即不超过容量j。这些所有满足条件:总体积=j的选法之和。
根据完全背包优化
题解
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010,mod = 1e9 + 7;
int n;
int f[N];
int main()
{
cin>>n;
f[0]=1;//这里很特殊,当n=0时当作1,而其他情况初始值是0,没有方案也作为一种方案,在有可行方案的时候没有方案不作为方案数。
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
f[j]=(f[j] + f[j-i])%mod;
cout<<f[n];
return 0;
}
方案二
分析
状态计算:f[i,j]所有可行方案分为:方案拆分成的数字最小值为1和最小值大于1两种,
若是最小值是1,即去掉这个1,总和变为i-1,需要j-1个数字,表示为f[i-1,j-1];
若最小值大于1,即对于每一个单独的方案,拆出来的每一个数减1,即总和减j,数字总量j保持不变,可表示为f[i-j,j],逆向思考,对于f[i,j]表示的所有方案每个组成的数字全部加1,就构成了总和为i,总数为j且最小值大于1的方案总数。
综上所述f[i,j]=f[i-1,j-1]+f[i-j,j] answer=f[n,1]+f[n,2]+...+f[n,n]
示例
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010,mod = 1e9 + 7;
int n;
int f[N][N];
int main()
{
cin>>n;
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=(f[i-1][j-1]+f[i-j][j])%mod;
int res=0;
for(int i=1;i<=n;i++) res = (res + f[n][i])%mod;
cout<<res;
return 0;
}
数位统计DP
例.AcWing338 计数问题
分析
设一函数f(n,x)可以算出1~n里数字x出现的次数。
以n=abcdefg,x=1为例:
以求1在第四位上出现的次数为例:
1<=xxx1yyy<=abcdefg
(1)xxx=000abc-1,yyy=000999,1出现的总数为abc*1000
(2)xxx=abc,从三种情况取其一
2.1) d<1,abc1yyy>abc0efd,无需继续考虑,总数为0
2.2) d=1,yyy=000~efg-1,总数为efg+1
2.3) d>1,yyy=000~999,总情况数为1000
故总情况数为(1)+(2)
边界情况:
1)x=1为例,1出现在最高位时无需考虑(1)
2)x=0时,对于(1),xxx显然不能取000,应从001开始取,也就是(abc-1)*1000
题解
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int get(vector<int> num,int l,int r)//得到第一种情况所需的“abc”是多少,第二种情况的“efg”是多少
{
int res=0;
for(int i=l;i>=r;i--)
res=res*10+num[i];
return res;
}
int power10(int x)//求10的i次方
{
int res=1;
while(x--) res*=10;
return res;
}
int count(int n,int x)
{
if(!n) return 0;
vector<int> num;
while(n)//将n的每一位拿出来,只不过是倒着的
{
num.push_back(n%10);
n/=10;
}
n=num.size();
int res=0;
for(int i=n-1-!x;i>=0;i--)//当x=0时最高位由于不可能是0所以要从第二位开始枚举,注意num是倒着的原数字
{
if(i<n-1)
{
res+=get(num,n-1,i+1)*power10(i);
if(!x) res-=power10(i);//x等于0特殊情况
}
if(num[i]==x) res+=get(num,i-1,0)+1;
else if(num[i]>x) res+=power10(i);
}
return res;
}
int main()
{
int a,b;
while(cin>>a>>b,a||b)
{
if(a>b) swap(a,b);
for(int i=0;i<10;i++)
cout<<count(b,i)-count(a-1,i)<<' ';
cout<<endl;
}
return 0;
}
状态压缩DP
特点:由于计算量大,所以数据范围不是很大。一般N<=20。
例.AcWing291 蒙德里安的梦想
核心
先放横着的,再放竖着的,横着的摆好后,竖着的只要依次填充,没有操作空间。
总方案数 = 等于只放横着的小方块的合法方案数
=>如何判断,当前方案是否合法?合法:竖着的小方块能恰好塞满剩余空间
=>由于所有的小方块都是竖着的,所以一列一列看,只要每列所有连续且空闲的方块连着数量为偶数即可
分析
状态计算:
分割f[i,j]枚举的是第i-1列伸出的情况,每行都有伸或不伸两种情况,用二进制压缩的k表示伸的情况以表示一个f[i,j]的子集 f[i-1,k] ,但由于第i-2列已经固定,所以i-1列不是每行都能放横着的块,故最坏的情况是每行都可以选择放或者不放横着的块,最坏情况数2^n。
但同时需要考虑,k与j要构成合法方案才能进行状态转移
k与j的合法方案要满足的条件:
(1) k&j == 0,即i-1这一列的格子不能同时被k与j设为1同时占据
(2) 一列中所有空着的位置的长度必须是偶数,保证竖着的小方块恰好能填满空闲
答案即为f[m,0] (从0开始表示第一列),按照之前的理解是整个第m+1列都是0,即没有伸出来到m+1列的,且m+1列之前的m列全部都是排满且合法的
题解
#include <cstring>
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 12,M = 1 << N;
int n,m;
LL f[N][M];
vector<int> state[M];//预处理所有状态state[i](也就是j)能够从哪些状态(也就是k)转移过来
bool st[N];//判断某个状态是否合法,即当前这一列的状态i(状压)是否满足每个连续空位为偶数
int main()
{
while(cin>>n>>m,n||m)
{
for(int i=0;i<1<<n;i++)//预处理某个状态是否合法,cnt就是计数连续方格数量
{
int cnt=0;
bool is_valid = true;
for(int j=0;j<n;j++)
{
if(i>>j&1)//判断第j位是不是1
{
if(cnt&1)//二进制数第一位是0是1说明了奇偶性
{
is_valid = false;
break;
}
cnt=0;
}
else cnt++;
}
if(cnt&1) is_valid = false;//特判最后一段空白是不是奇数个格子
st[i]=is_valid;
}
for(int i=0;i<1<<n;i++)//预处理状态i对应的所有可以进行转移的状态
{
state[i].clear();
for(int j=0;j<1<<n;j++)
{
if((i&j)==0 && st[i|j])//检查合法方案的两个条件
state[i].push_back(j);
}
}
memset(f,0,sizeof f);
f[0][0]=1;//通常起始状态没有方案为1
for(int i=1;i<=m;i++)
for(int j=0;j<1<<n;j++)//对于所有状态j
for(auto k:state[j])//遍历所有合法上一个状态k
f[i][j]+=f[i-1][k];
cout<<f[m][0]<<endl;
}
return 0;
}
例.AcWing91 最短Hamilton路径
分析
状态计算:
分割子集依据:倒数第二个点是什么,分别是0、1...n-1;
假设倒数第二个点其中一个是k,0->k->j,即集合i除去j这个点的f[i-{j},k],再加上k到j的举例a[k,j],枚举这样的k,对刚刚的表达式取min
题解
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 20,M = 1<<N;
int n;
int w[N][N];
int f[M][N];
int main()
{
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>w[i][j];
memset(f,0x3f,sizeof f);
f[1][0]=0;
for(int i=0;i<1<<n;i++)
for(int j=0;j<n;j++)
if(i>>j&1)//i集合中要含有j这个点
for(int k=0;k<n;k++)//枚举所有j点的前一个点
if((i-(1<<j))>>k&1)//满足条件的k才是合法的,要求k属于i且不是j
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
cout<<f[(1<<n)-1][n-1]<<endl;
return 0;
}
树形DP
例.AcWing285 没有上司的舞会
分析
状态计算:
f(u,0),因为没有选点u,所以其子节点Sn可选可不选,所以选最大值,即f(u,0)=...+max( f(Sn,0),f(Sn,1) )+...
f(u,1),因为选了点u,所以首先先加上点u的权,然后所有的子节点Sn都不能选,故f(u,1)=w[u]+...
+f(Sn,0)+...
题解
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 6010;
int n;
int happy[N];
int h[N],e[N],ne[N],idx;
int f[N][2];
bool has_father[N];
int add(int a,int b)
{
e[idx]=b;
ne[idx]=h[a];
h[a]=idx;
idx++;
}
void dfs(int u)//从根节点遍历每个子节点,广度优先
{
f[u][1]=happy[u];//f[u][1]因为选了u点自身,上来要先加上自己
for(int i=h[u];i!=-1;i=ne[i])
{
int j = e[i];
dfs(j);
f[u][0]+=max(f[j][0],f[j][1]);
f[u][1]+=f[j][0];
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&happy[i]);
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++)
{
int a,b;//b是a的父节点
scanf("%d%d",&a,&b);
has_father[a]=true;
add(b,a);
}
int root=1;
while(has_father[root]) root++;
dfs(root);
printf("%d\n",max(f[root][0],f[root][1]));
return 0;
}
记忆化搜索
本质上是对DP的一种更简单的理解
代码的形式有所变化,但核心思想与DP一样
例.AcWing901 滑雪
分析
状态计算:
按往哪滑分成四类,但不是一定可行,可行的条件是下一个格子的高度低于现在的高度
不同于一般DP,这题提供一种递归写法作为启发示例
记忆化搜索写起来要比循环更好写更好理解,但可能会溢出。
题解
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int n,m;
int f[N][N];
int h[N][N];
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
int dp(int x,int y)
{
int &v=f[x][y];//引用,简化代码
if(v!=-1) return v;
v = 1;//这个v=1最为重要,一步一步递归深入,找到最开始的起点,将其设为1
for(int i=0;i<4;i++)
{
int a=x+dx[i],b=y+dy[i];
if(a>=1 && a<=n && b>=1 && b<=m && h[a][b]<h[x][y])
v=max(v,dp(a,b)+1);
}
return v;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&h[i][j]);
memset(f,-1,sizeof f);//初始化,表明没有被算过
int res=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
res=max(res,dp(i,j));
printf("%d\n",res);
return 0;
}

浙公网安备 33010602011771号