动态规划简介(未完)
普通dp
普通dp是指一些很浅显的、可以由无后效性和最优子结构得出答案的题目
非常基础的题目:
P1216 数字三角形
1 |
|
记忆化搜索+dp(较难)
dp跟记忆化搜索的关系十分密切
而记忆化搜索的主要难点并不在于dp,而在于DFS。
因此虽然题目可能较为困难,但dp的思路十分显然。
记忆化搜索就是,当一个点的结果可以由子结构的结果直接给出,那么我们就利用这一特点,用一种类似于dp的形式去搜索,避免反复搜索同样的结构即可。
P1113 杂务
思路:
这题一看就是拓扑排序。
考虑反向建边+dfs的dp,同《图的遍历》
DAG可直接用DFS+dp,不像“图的遍历”,还要调整顺序
拓扑排序的DFS做法好像就是DFS本体加上递归之前的一些操作?
最大值必定是树形结构的尽头
1 |
|
P4017 最大食物链计数
事实上,拓扑排序带给我们的最大启示就是要有寻找0入度的点的意识
在此题,我们可以通过读入边时标记入度,最后直接遍历全部点得到入度为0的点
记忆化就是搜索的神
善用记忆化搜索,怎么搜都不怕爆了
1 |
|
P1434 [SHOI2002] 滑雪
1 |
|
背包dp
01背包
此为第三次做,代码能力明显提升,代码精简,思路清晰1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using namespace std;
int t,m,times[105],values[105];
int dp[105][1005];
int main(){
cin>>t>>m;
for(int i=1;i<=m;i++){
cin>>times[i]>>values[i];
}
for(int i=1;i<=m;i++){
for(int j=1;j<=t;j++){//dp[i][j]是指前i件,用j钱的最优解
if(j>=times[i]){//买得起当前件
dp[i][j]=max(dp[i-1][j],dp[i-1][j-times[i]]+values[i]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
cout<<dp[m][t];
return 0;
}
尝试转化成一维滚动数组:
代码更短。
事实上,如果掌握了普通的二维背包,转化成一维滚动数组完全不难。
只需注意到,对于$j$需要从后往前遍历,以实现更新时获取的都是上一次的值。避免左脚踩右脚上天1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using namespace std;
int t,m,times[105],values[105];
int dp[1005];
int main(){
cin>>t>>m;
for(int i=1;i<=m;i++){
cin>>times[i]>>values[i];
}
for(int i=1;i<=m;i++){
for(int j=t;j>0;j--){//dp[i][j]是指前i件,用j钱的最优解
if(j>=times[i]){//买得起当前件
dp[j]=max(dp[j],dp[j-times[i]]+values[i]);
}
// else{
// dp[j]=dp[j];
// }
}
}
cout<<dp[t];
return 0;
}
完全背包
P1616 疯狂的采药
完全背包模板:
暴力法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using namespace std;
int t,m;
const int MAXN=1e4+5,MAXT=1e7+5;
int values[MAXN],times[MAXN];
int dp[MAXN][MAXT];
int main(){
cin>>t>>m;
for(int i=1;i<=m;i++)
cin>>times[i]>>values[i];
for(int i=1;i<=m;i++){
for(int j=1;j<=t;j++){//dp[i][j]表示在前i件物品中,用t的钱的最优解
//完全背包,不知道要买几件
//尝试枚举买的数量
if(j<values[i]){
dp[i][j]=dp[i-1][j];
continue;
}
dp[i][j]=dp[i-1][j];
//先储存不买的结果
for(int k=1;k<=j/times[i];k++){//买k件
dp[i][j]=max(dp[i][j],dp[i-1][j-k*times[i]]+k*values[i]);
}
}
}
cout<<dp[m][t];
return 0;
}
暴力法思路较为清晰,但是所需空间太大,大概是100GB
$(1GB对应2.7 \times 10^{8}个int类型)$
空间优化:滚动数组1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17int main(){
scanf("%d%d",&t,&m);
for(int i=1;i<=m;i++)
scanf("%d%d",×[i],&values[i]);
for(int i=1;i<=m;i++){
for(int j=t;j>=1;j--){
if(j<times[i]){
break;
}//
for(int k=1;k<=j/times[i];k++){//买k件
dp[j]=max(dp[j],dp[j-k*times[i]]+k*values[i]);
}
}
}
cout<<dp[t];
return 0;
}
时间优化:
解决时间问题:
在选定的i上,假设我现在可以购买k件,那么最优解从何得出呢?
是从买0件,买1件,买2件…买k-1件的最优解x与买不买第k件的最优解得出的。
而前k-1件的最优解同样符合此规律(最优子结构)
因此,状态转移方程是 $dp[j]=max(dp[j],dp[j-times[i]]+values[i])$
核心逻辑就是每次加钱,都比较一下这个时候买和这个时候不买会怎么样。
如果 j>=times[i]且j<2*times[i] ,那么就产生 买一件和不买 最优解
如果j>=2*times[i],那么就产生买两件和买一件的最优解
简而言之,完全背包就是和同层的点去比,因为需要获取可能已经买了物品$i$的结果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using namespace std;
int t,m;
const int MAXN=1e4+5,MAXT=1e7+5;
long long dp[MAXT];
int values[MAXN],times[MAXN];
int main(){
scanf("%d%d",&t,&m);
for(int i=1;i<=m;i++)
scanf("%d%d",×[i],&values[i]);
for(int i=1;i<=m;i++){
for(int j=1;j<=t;j++){
if(j<times[i])
continue;
dp[j]=max(dp[j],dp[j-times[i]]+values[i]);
}
}
cout<<dp[t];
return 0;
}
非完全背包
P1077 [NOIP 2012 普及组] 摆花
非完全背包的优化思路较难,暂时没有精力写
此题数据量较小,无需优化也可通过
从此题可以看出,多次背包的精髓在于,多开了一个选择的维度,相当于N个01背包的加和1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using namespace std;
const int MOD=1e6+7;
int n,m,flower[105];
int dp[105][105];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>flower[i];
dp[i][0]=1;
}
dp[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
//类完全背包,需要枚举买的数量
dp[i][j]=dp[i-1][j];
for(int k=1;k<=flower[i];k++){//在这里买k个
if(j<k)
break;
dp[i][j]+=dp[i-1][j-k];
dp[i][j]%=MOD;
}
}
cout<<dp[n][m];
return 0;
}