将问题所给的过程,划分为若干相互联系的阶段
一个阶段包含若干的状态
决策就是每次选择性的行动。即从一个阶段的某一状态转换为另一阶段的某一状态。
线性模型即状态的排布是呈线性的。
DP中最常用的模型。
数组a:给定序列
dp[i]:表示必须以a[i]截止的最长单调递增子序列的长度。
状态转移方程:dp[i]=max(dp[j]+1) (a[i]>a[j],1<=j 边界条件: 以a[1]截止的序列长度就为1 dp[1]=1;
代码实现:
#include
#include
#include
using namespace std;
int main(){
int n,a[105],m=0;
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
//确定状态
int dp[105]={0};
//dp[i]:必须以a[i]截止的最长递增子序列
//边界条件:
//以a[1]截止的序列长度就为1
dp[1]=1;
//dp:
for(int i=2;i<=n;i++){
//保存最大值
int k=0;
for(int j=1;j<i;j++){
//如果递增并且长度大于当前的最大值
//正推(递推) dp[j]都在前面求出来了
if(a[i]>a[j]&&dp[j]>k)
//更新当前最大值
k=dp[j];
}
//长度为当前保存最大值+1
dp[i]=k+1;
//边求边更新最大长度
if(m<dp[i])
m=dp[i];
}
printf("%d",m);
return 0;
}
经典背包问题:
N件物品,每件物品重量为wi,价值为ci,背包负重为V。求在背包容量一定条件下,使装入背包的物品的价值最大。
背包问题经典模型:0-1背包、完全背包、多重背包。
0-1背包:N件物品中每件物品只有一件要么装,要么都不装。
完全背包:N件物品中每件物品有无限件。
多重背包:N件物品中第i件物品有n[i]件。
状态:
dp[i][j]:在背包容量为j下,选择前i件物品 获得的最大价值。
状态转移方程:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i])
考虑第i件物品选和不选,哪个产生的价值更大。
代码实现:
#include
#include
using namespace std;
int main(){
int n,w[105],c[105],dp[105][105]={0},v;
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d",&w[i],&c[i]);
//初始化:
//如果第一个物品的重量大于背包体积则无法装入,否则就能装
for(int i=0;i<=v;i++) if(w[1]<=i) dp[1][i]=c[1];
//dp:
for(int i=2;i<=n;i++){
for(int j=0;j<=v;j++){
if(w[i]<=j){
//第i件 要么选 要么不选
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i]);
}
}
}
printf("%d",dp[n][v]);
return 0;
}
空间优化:将二维数组转化为一维数组
与二维数组类似,就是每次计算的值都覆盖上一次的值。
dp[j]:当前阶段背包体积为j时,所能装的物品的最大价值。
dp[j]=dp[j-w[i]]+c[i]
!!!但是注意,必须要逆推! 即体积:V~0。这样做保证了每个物品只能被取一次。
因为更新j一定要来自于j-w[i],如果正推的话j-w[i]已经被更新过了,就会导致数据错误!!!
代码实现:
#include
#include
using namespace std;
int main(){
int n,w[105],c[105],dp[105]={0},v;
scanf("%d%d",&n,&v);
for(int i=1;i<=n;i++) scanf("%d%d",&w[i],&c[i]);
//dp:
for(int i=1;i<=n;i++){
for(int j=v;j>=0;j--){
if(w[i]<=j){
//第i件 要么选 要么不选
dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
}
}
}
printf("%d",dp[v]);
return 0;
}
每种物品有无穷件。
状态定义与0-1背包一样,但是状态转移方程有所不同
dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*w[i]]+k*c[i]) (0<=k*w[i]<=j)
时间复杂度很高,一样还要枚举k,所以我们可以类比0-1背包空间优化,来优化完全背包。
这里有个很巧妙的方法,当将0-1背包空间优化中的体积正着枚举就是完全背包了!太巧妙了!!!
你有点不信?我们看例子:
f[j]=max(f[j],f[j-w[i]]+v[i])
当计算前i件物品,背包体积为j时,我们计算f[j-w[i]]:此时f[j-w[i]]已经被更新过了,即为 在前i件物品选择,背包体积为j-w[i]。注意:它已经有可能包含了第i件物品!(它也有可能不取)
例如:
重量 价值
1 3
2 5
3 4
计算第二件物品:
dp[2]=5
dp[4]=dp[2]+5=10 选了两件2物品
dp[6]=dp[4]+5=10+5=15 3件2物品
计算第三件物品时:
dp[3]=dp[3-3]+4=4 选了第三件物品
dp[6]=dp[6-3]+4=dp[3]+4=8 选了两件第三件物品。
而我们使用逆推 0-1背包:
第一件物品:dp[1~6]=3
计算第二件物品:
dp[6]=dp[4]+5=3+5=8 这里dp[4] dp[2]都是选前一件物品的价值,
dp[3]=dp[1]+5=8 选了一件1物品 一件2物品
计算第三件物品
dp[6]=dp[6-3]+4=8+4=12 选了一件1物品 一件2物品 一件3物品
看出差别了吗?
例子:看这篇博客的例子,讲的很详细了。
Money System
区间DP,即在区间中做动态规划。先得到小区间最优解再得到大区间的最优解。
一般的状态都为dp[i][j]:在区间i j上的最优解。
题目简述:n堆石子,第i堆有a[i]个,合并时合并相邻两堆,产生的代价为两堆果子的总数,求合并成一堆后,最小代价。
输入: 3 1 2 3
输出:
9
解释:先合并1 2 代价为1+2=3 ,此时 石子为 3 3,再合并3 3 代价为3+(3+3)=9
如果先合并 2 3 代价为2+3=5 石子为 1 5 代价 5+(1+5)=11*
状态 :dp[i][j]:合并i j 区间所用的最小代价。
由于一堆肯定是由两堆来的 划分:dp[i][k],dp[k+1][j],即合并的代价为 a[i]+a[i+1]+……+a[j] 。所以,我们设sum[i]为a[1]到a[i]的和 即前缀和,
因为他只能相邻的两堆合并。
状态转移方程:dp[i][j]=min(dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]) (i<=k<=j)
边界条件:当i等于j时,自己不用合并代价为0.
代码实现:
#include
#include
#include
using namespace std;
int main(){
int n,a[105],sum[105]={0};
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
sum[i]=sum[i-1]+a[i];
}
int dp[105][105]={0};
//枚举区间长度
for(int len=2;len<=n;len++){
//左端点 (右端点长度不能超过n)
for(int i=1;i+len-1<=n;i++){
//右端点
int j=i+len-1;
//先将代价设置为最大
dp[i][j]=0x3f3f3f3f;
for(int k=i;k<j;k++){
//取 i-j 代价最小的情况
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
}
}
}
printf("%d",dp[1][n]);
return 0;
}
解释:我们一定要先从计算小区间,然后再计算大区间! 大区间状态一定要来自于小区间的状态。
这类问题一般统计某个区间的数字符合某种条件。
例如:在[L,R]区间中,看有多少个数字符合能被7整除,并且数中含有3。
一般L R的规模都非常的大。
数位DP我们要考虑数字中的每一位。我们从高位向低位枚举(高位数范围决定低位数)。
例如 N=1024。
假设我们前两位枚举的是10,那么第三位只能选0~2。
如果枚举前三位是102,那么第四位只能选0~4。
技巧:
1、将区间变换成一个界限 [x,y]=f(y)-f(x-1)
2、从树的角度来考虑
烦人的数学作业
题解等我研究研究,补上
树形动态规划。状态图为一棵树。状态转移也发生在树上。父节点的值都通过子节点得到。
给定一棵树,边有权,计算一条最长链,要求时间复杂度为O(n)
找某个结点的最长链,就找它所有孩子的最长链,然后加上与这个孩子的边权值。
dp[i]:表示以i为根的子树中的最长链。
dp[i]只有两种情况:一种是以i为根结点最长,另一种是i为中间结点
i为根结点
i为中间结点
第一种情况即为树高问题很好解决,关键是第二种情况啊。
设状态dp[u][1/0]:0表示包括u的最长链,1表示包括u的次长链。二者应该严格不等。即路径没有重叠。
如果为第一种情况,那么dp[u][0]即为最长
第二种情况 最长链 dp[u][0]+dp[u][1]-1(计算了两次u)
dp[u][0]=max(dp[v][0]+1) v∈son(u)
dp[u][1]:如果为叶子则为最小值,否则为secmax(dp[v][0])+1
secmax():取次大。
如何求次大呢?我们在遍历的时候dp[v][0]时,看其与dp[u][0]和dp[u][1]的关系,如果大于dp[u][0] 那么 dp[v][0]为 替换dp[u][0] dp[u][0]替换dp[u][1]
如何仅大于dp[u][1] 那么就替换dp[u][1]。
DP的核心在于状态的定义。
对于不易表示的状态,我们可以考虑使用状态压缩的思想来优化求解过程。
问题描述:旅行家在n个点m条边的有向图中,从一个点出发,各个城市经历一次且仅一次,然后回到出发城市,求路径最短
我们分析题目可知,我们需要时刻知道 旅行家在哪个点,它经过了哪些点。
第一个很好维护。
第二个是一个集合,有2^n可能性,不太好表示。
我们可以将一个集合映射成 [0~2^n-1] 的数字, [0~2^n-1]范围的数字二进制表示最多为n位。
假设n=5.则
{1,3,5}=10101
{2}=00010
定义状态:dp[i][j]:表示所在点为i,已经经过点的集合对应下标为j时所走最短路径。
状态转移方程:
对于一条x到k代价为c的边
使用移位运算和按位与和按位或运算。
//先判断k点是否走过,
//判断j的第k-1位 是否等于0 等于0则没走过
if(j&(1<<(k-1))==0)
// | 按位或
dp[k][j|1<<(k-1))]=dp[i][j]+c
先只会思想吧。