世界,赋予了我|

fufufuf

园龄:1个月粉丝:0关注:1

线性DP以及背包问题讲解

今天就主要是讲一下线性DP和状态压缩这两种动态规划的知识,这个是比较常见的两种动态规划的思路(瑞平:作者还不把之前的算法给更新一下)

首先要讲的是动态规划这一概念,这里面动态规划是一种思路,即只在乎当前的状态,以及现在这一状态是怎么转移过来的,这样子的一个算法思路,那么来看一下,对于一个问题,我们怎么判断他是一个DP的问题呢,可以从以下的三个角度出发

1.无后效性:

对于已经求结过的子问题,后续的决策不会对当前的决策产生影响。

2.子问题重叠:

子问题 A 和子问题 B 可能存在共同的子问题 C,那么我们可以将一些重叠的子问题存储下来,特别来说,重叠的越多,我们的空间利用率越高。

3.最优子结构:

当前的问题一定可以最优解一定可以通过子问题的最优解导出。如(三角形取数,可以参考上一篇动态规划的博客)

那么接下来来看一下线性DP

其实还是比较喜欢用题目来引入,这样子比较方便,首先是一个基础题目,不熟悉动态规划的朋友可以看一下
https://leetcode.cn/problems/climbing-stairs/description/
(不会有人这道题还要我提供代码吧)

然后下面这个题目是进阶版的,可以同样来参考一下。
https://www.luogu.com.cn/problem/P1002
下面是这道题的代码

Python:

x1,y1,x2,y2=map(int,input().split())
dp=[[0]*(x1+2) for _ in range(y1+2)]
dp[1][1]=1
x1+=1
x2+=1
y1+=1
y2+=1
ma=[(x2,y2),(x2-2,y2-1),(x2-1,y2-2),(x2+1,y2-2),(x2+2,y2-1),(x2+2,y2+1),(x2+1,y2+2),(x2-1,y2+2),(x2-2,y2+1)]
 
for x in range(1,x1+1):
    for y in range(1,y1+1):
        if x==1 and y==1:
            continue
        elif (x,y) in ma:
            continue
        else:
 
            dp[y][x]=dp[y][x-1]+dp[y-1][x]
print(dp[-1][-1])

C++

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
 
const int fx[] = {0, -2, -1, 1, 2, 2, 1, -1, -2};
const int fy[] = {0, 1, 2, 2, 1, -1, -2, -2, -1};
//马可以走到的位置
 
int bx, by, mx, my;
ll f[40][40];
bool s[40][40]; //判断这个点有没有马拦住
int main(){
    scanf("%d%d%d%d", &bx, &by, &mx, &my);
    bx += 2; by += 2; mx += 2; my += 2;
    //坐标+2以防越界
    f[2][1] = 1;//初始化
    s[mx][my] = 1;//标记马的位置
    for(int i = 1; i <= 8; i++) s[mx + fx[i]][my + fy[i]] = 1;
    for(int i = 2; i <= bx; i++){
        for(int j = 2; j <= by; j++){
            if(s[i][j]) continue; // 如果被马拦住就直接跳过
            f[i][j] = f[i - 1][j] + f[i][j - 1];
            //状态转移方程
        }
    }
    printf("%lld\n", f[bx][by]);
    return 0;
} 

这个是两个比较基础的线性动态规划

在这之后,就是一个比较正常难度的线性动态规划题目。
https://www.lanqiao.cn/problems/3601/learning/
那么先来分析这道题,首先是无后效性,青蛙在当前位置吃完虫子之后,就会前往下一个位置,这里面在下一个位置吃的虫子不会对当前位置吃的虫子产生影响,所以是可行的;然后是最优子结构,最优子结构的话,这个青蛙只要在接下来的情况中去最大的节点去取吃虫子就可以了,那么怎么去设计状态呢,这里面我们要求的是最多的虫子食用数量,所以可以那这个作为DP的结果,在这之后,就是去看这个状态是怎么设计,这里面可以考虑角度,来描述状态,首先考虑青蛙所在的位置,但是仅仅只有位置没有办法描述青蛙的移动,因为你只是知道青蛙在哪里结束,但是不知道青蛙是怎么到这里的,所以还要增加一个状态,即青蛙到达这个位置花了几步,所以在这里便可以开始设计状态转移,通常,会使用一个数学的表达式来表示状态转移,

那么在这里,状就是当青蛙在位置i的时候,已经走了j步,这时青蛙获取虫子的最大值,这个便是用dp[i][j]来表示的一个状态。

那么便可以得出下列状态转移方程

根据这个状态转移的方程,就可以去处理了,然后这里面需要注意的一点:

初始化dp数组的时候一定要用-0x3f3f(Python是 -math.inf ),而不是去使用0,不然会WA,因为要确保每一处应该更新的都要被更新,这里面 -0x3f3f3f->0和 0->0是两个截然不同的过程,因此需要注意,一般的其实只要碰到有最值的情况就直接改为最小值,个别特殊情况就要去使用-0xe3f3f3f3f这样更小的数字,后面会讲到一个这样子的题目(当时调试了半天,不知道怎么回事)
Python

t=int(input())
for i in range(t):
    ans=0
    n,a,b,k=map(int,input().split())
    A=list(map(int,input().split()))
    dp=[[0 for x in range(n+1)] for y in range(k+1)]
    for i in range(1,k+1):#走了i次
        for j in range(n+1):#现在在哪个位置
            if j>i*b:
                break
            if j<i*a:
                continue
            for v in range(a,b+1):
                if j>=v:
                    dp[i][j]=max(dp[i][j],dp[i-1][j-v]+A[j-1])
                else:
                    dp[i][j]=dp[i][j]
        ans=max(ans,max(dp[i]))
    print(ans)

C++

#include <bits/stdc++.h>
#define MAXN (int)103
using namespace std;
 
int T;
int N, A, B, K;
int a[MAXN];
int dp[MAXN][MAXN];
 
void sol() {
	cin >> N >> A >> B >> K;
	for (int n = 1; n <= N; n++) {
		cin >> a[n];
	}
	int ans = 0;
	memset(dp, -0x3f3f, sizeof(dp));
	dp[0][0] = 0;
	for (int j = 1; j <= N; ++j) {
		for (int i = 1; i <= K; ++i) {
			for (int k = A; k <= B; ++k) {
				if (j - k < 0)continue;
				dp[i][j] = max(dp[i][j], dp[i - 1][j - k] + a[j]);
 
			}
			ans = max(ans,dp[i][j]);
		}
	}
	cout << ans << endl;
}
 
int main() {
	cin >> T;
	while (T--) {
		sol();
	}
	return 0;
}

然后就是线性动态规划里面比较经典的模型,背包问题,在这篇文章里面会讨论的主要是多重背包问题(你问作者为什么不讲其他的背包问题,因为这样子可以再写一篇文章,好像欠的文章越来越多了)

下面可以看一个板子,这个就是多重背包
https://www.lanqiao.cn/problems/1177/learning/
多重背包是背包问题的一种,他是去求出在有限制条件的情况下的最大值,那么,就要去讨论取不取,取多少的问题,在这里,就涉及到了怎么去取物品的问题。

在这道题中,对于一件物品,我们要去讨论取多少个,这个怎么处理是第一个要解决的问题,那么这里涉及到的思想叫做二进制拆分,当然,如果你一个一个的决定取多少个也是可以解决的,不过对于空间和时间的要求就会更加不乐观,那么二进制拆分就可以解决这个问题,就比如如果我现在有五个物品,我要去1个,取2个,取3个,取4个我该怎么办。

可以见下面的解法。

那么在这里,我是用如此方法去拆分,就可以表示所有的数字了,类似的,我可以9拆分成4,2,2,1这4个数字,同样可以使用它们组合成小于9的任何数字。那么,我们相当于用这种方式将数字给拆分开了就可以使用01背包来解决了,这样子,总数也只是扩大了logN倍而不是N倍,时间和空间的需求不会特别的增加,所以就可以避免TLE和MLE的发生。

那么剩下的,我就是对于每一个物品,去讨论他是否可以取样就可以了,我在这里对于状态的设计为dp[i][j]表示对于第i个物品,质量为j的时候的总价值是多少,这样子就可以得到状态转移的方程

当第i个物品不取样的时候:

当第i个物品取样的时候

(这个公式编辑器真蠢)

那么就可以写出代码了

C++

#include<bits/stdc++.h>
#define ll long long
#define MAXN (int)1e3+10
using namespace std;
 
ll N, V;
struct node {
	ll w, v, s;
} a;
 
int max(int a, int b) {
	if (a > b) {
		return a;
	}
	return b;
}
 
struct node2 {
	ll w, v;
}b;
int main() {
	cin >> N >> V;
	vector<node> nodes;
	vector<node2> new_nodes;
	for (int i = 1; i <= N; i++) {
		cin >> a.w >> a.v >> a.s;
		if (a.s == 0) {
			a.s = V / a.w;
		}
		nodes.push_back(a);
	}
	new_nodes.push_back(b);
	//对每一项进行二进制拆分
	for (node i : nodes) {
		int n = i.s;
		int num = 1;
		while (num < n) {
			b.v = i.v * num;
			b.w = i.w * num;
			n -= num;
			num *= 2;
			new_nodes.push_back(b);
		}
		b.w = i.w * n;
		b.v = i.v * n;
		new_nodes.push_back(b);
	}
 
	/*for (node2 i : new_nodes) {
		cout << i.v << " " << i.w << endl;
	}*/
 
	//进行动态规划,使用多重背包
	int dp[2][2003];
	memset(dp, 0, sizeof(dp));
	/*for (int i = 1; i < new_nodes.size(); i++) {
		for (int j = 0; j <= V; j++) {
			cout << dp[i][j] << " ";
		}
		cout << endl;*/
	//}
	for (int i = 1; i < new_nodes.size(); i++) {
		for (int j = 0; j <= V; j++) {
			if (j - new_nodes[i].w < 0) {
				dp[i%2][j] = dp[(i - 1)%2][j];
			}
			else {
				dp[i % 2][j] = max(dp[(i - 1) % 2][j], dp[(i - 1) % 2][j - new_nodes[i].w] + new_nodes[i].v);
			}
		}
		
	}
	cout << dp[(new_nodes.size() - 1)%2][V] << endl;
}

至于为什么这里会出现一个%2,这里面我们可以注意到,对于这个状态转移方程来说,第i行的情况只是有第i-1行决定的,所以说对于这里面我们只用看上一行就可以了,上上一行即第i-2不重要,所以说就只用两行就可以了(为什么不用滚动数组,因为这样子更加方便,而且大多数的题目不会去卡这个内存)。

最后一个就是关于蓝桥杯的真题(顺便一提,蓝桥杯真的很好拿省奖,真的就是有手就行)

李白打酒
https://www.luogu.com.cn/problem/P8786
不同于之前的dp去求值,这里面是求方案数,这个也是一种DP的考法,一般常见的就是两种,一种是去求解最值,另一种则是去求解方案数,李白打酒这一题就是去求解方案数的。

那么这道题里面,有两种东西,一个是花,一个是酒店,那么这里面就是对花和酒店来做处理,然后来看怎么描述状态,但是这里面是讨论方案数的事情,那么就会出现一个问题,就是如果只有两个变量来记录当前走多多少花与酒店,这是无法满足需求的,怎么办,那就是去添加新的状态,就在添加添加一个变量,来表示自己在经过了i家酒店和j家花之后,还有k的酒的方案数,那么接下来就可以列出状态转移方程了

遇到花

遇到酒

#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll mod = (ll)1e9 + 7;
 
int N, M;
int dp[105][105][105];
 
int main() {
	cin >> N >> M;
	memset(dp, 0, sizeof(dp));
	dp[0][0][2] = 1;
	for (int i = 0; i <= N; i++) {
		for (int j = 0; j < M; j++) {
			if (i == 0 && j == 0) {
				continue;
			}
			for (int k = 0; k < M; k++) {
				if (j > 0) {
					dp[i][j][k] += dp[i][j - 1][k + 1];
				}
				if ((k % 2 == 0) && i > 0) {
					dp[i][j][k] += dp[i - 1][j][k / 2];
				}
				dp[i][j][k] %= mod;
			}
		}
	}
	cout << (dp[N][M-1][1]%mod) << endl;
}

这里面只要j>0就是遇到花了,所以就要从j-1转移过来,同样的,只要i>0且酒的数量偶数的时候就说明喝了酒,因为要加了一半的酒,因为偶数2是偶数,奇数2还是偶数,所以在这里可以知道,只要是偶数就可以讨论出现酒店的情况,当然,在k为偶数的时候并不一定是只从酒店转移过来,也有可能是从花转移过来的。所以这里面的转移是没有问题的。

由于题目说了最后遇到的是花,所以说我们对于花要少讨论一朵,而对于酒的数量,要计算到只有一斗的时候就可以了。

就到这里了(作者又不想写结尾了)。

本文作者:fufufuf

本文链接:https://www.cnblogs.com/fufufuf/p/18669377

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   fufufuf  阅读(8)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起