2021-09-19 第一篇博客 动态规划1

[动态规划1] 动态规划的引入 第一篇博客

  • 动态规划概述
  • 动态规划初步练习
    • 洛谷P1216 数字三角形
    • 洛谷P1434 滑雪
    • 洛谷P2196 挖地雷 (输出路径)
    • 洛谷P4017 最大食物链计数(拓扑排序)
    • 洛谷P1048 采药(0-1背包)
    • 洛谷P1616 疯狂地采药(完全背包)
    • 洛谷P1802 五倍经验日(变异的0-1背包)
    • 洛谷P1002 过河卒(像青蛙跳台阶)

动态规划概述

关键词:
状态转移方程 全局最优化 无后效性
背包问题 线性DP 区间DP 树形DP 状态压缩DP

动态规划初步练习

洛谷P1216 数字三角形

数字三角形
题目要求第一层到最后一层的路径和最大,可以从最后一层开始往上走。
最后一层:

dp[n][i] = a[n][i];

逐层向上,因为每一步可以往左下方或者右下方走,所以状态转移方程为:

dp[i][j] = a[i][j] + max(dp[i+1][j],dp[i+1][j+1]);

最后 dp[1][1] 即为结果。
完整代码:

#include
#include
#include
using namespace std;
int a[1005][1005],dp[1005][1005];
int main()
{
	int n;
	cin>>n;
	memset(a,0,sizeof(a));
	memset(dp,0,sizeof(dp));
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= i; j++)
			cin>>a[i][j];
	//自下而上 
	for(int i = 1; i <= n; i++)   dp[n][i] = a[n][i];
	
	for(int i = n-1; i >= 1; i--)     
		for(int j = 1; j<= i; j++)
			dp[i][j] = a[i][j] + max(dp[i+1][j],dp[i+1][j+1]);   //状态转移方程 
			
	cout<

洛谷P1434 滑雪

滑雪
网上题解看到多用的记忆化搜索或者是DP,考虑到是动态规划专题,还是学学DP的做法,其实搜索也没学好,改日再补orz。
dp[x][y] 用来存储从点x,y出发所能达到的最长路。
状态转移方程:

s = max(s,DP(nx,ny)+1);

DP(nx,ny) 直到最后一个不能往下走的点才会结束,显然这个点的 dp[x][y] 为1,其相邻结点的 dp[x][y] 最少为2,逐步可以推出初始点x,y的 dp[x][y]

#include 
#include
using namespace std;
int a[101][101];
int dp[101][101];
int dx[4] = {1,-1,0,0};
int dy[4] = {0,0,1,-1};
int r,c;
int DP(int x,int y)
{
	if(dp[x][y]) return dp[x][y];
	int s = 1;
	int nx,ny;
	for(int i = 0; i < 4; i++)
	{
		nx = x + dx[i];
		ny = y + dy[i];
		if(nx<0 || nx >= r || ny<0 || ny >= c)
			continue;
		if(a[x][y] > a[nx][ny])
			s = max(s,DP(nx,ny)+1);
	}
	dp[x][y] = s;
	
	return dp[x][y];
}
int main()
{
	cin>>r>>c;
	for(int i = 0; i < r; i++)
		for(int j = 0; j < c; j++)
		{
			cin>>a[i][j];
			dp[i][j] = 0;
		}
	int ans = 0;
	
	for(int i = 0; i < r; i++)
		for(int j = 0; j < c; j++)
			ans = max(ans,DP(i,j));
	
	cout<

洛谷P2196 挖地雷 (输出路径)

挖地雷
这道题借用上一题 滑雪 的思路可以很轻松地求出最大地雷数,
只需修改 s 的初始值和状态转移方程即可,状态转移方程如下:

s = max(s,DP(i)+num[x]);

用上一题的思路的话难点在于 路径的输出 ,较为繁琐,如有更好的方法欢迎讨论,完整代码如下:

#include
using namespace std;

int n; 
int a[22][22];   //a[i][j]记录i和j之间是否通路 
int dp[22];    	 //dp[i]用来储存从i出发可挖最大地雷数
int num[22];     //每个地窖地雷数 
int p[22];       //记录路径 
int m;
int start;       //记录最优路径起点

void PRINT()
{
	m = 0;
	p[++m] = start; 
	dp[start] -= num[start];
	for(int i = start+1; i <= n; i++)
	{
		if(dp[start] == dp[i])
		{
			p[++m] = i;
			dp[start] -= num[i];
		}
	}
	
	for(int i = 1; i <= m; i++)	
		printf("%d ",p[i]);
	cout<>n;
	int x;
	int ans = 0;
	for(int i = 1; i <= n; i++)
		cin>>num[i];
	for(int i = 1; i < n; i++)
	{
		for(int j = i + 1; j <= n; j++)
		{
			cin>>x;
			a[i][j] = a[j][i] = x;
		}
	}
	
	for(int i = 1; i <= n; i++)
	{
		if(ans < DP(i))
			start = i;   //记录最优路径起点 
		ans = max(ans,DP(i));	
	}
	
	PRINT();
		
	cout<

洛谷P4017 最大食物链计数(拓扑排序)

最大食物链计数
初看感觉像并查集,但是又不太对,好像也不太能用DP做,看看题解.
看题解都说是DAG(有向无环图)计数问题,第一次见,好好学习.
题解大多用拓扑排序,不会,再看看有没有用DP做的.

算了,还是学习一下拓扑排序吧.
拓扑排序 (百度百科)

对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
2021-09-19 第一篇博客 动态规划1_第1张图片
文字与图源自百度百科。

用队列 q 来存储入度为0的结点(起初只有生产者),遍历入度为0的结点 a,出队,找寻以它为食物的所有生物,它们的入度减1,更新食物链,当入度为0且出度不为0时入队,重复上述过程.
完整代码如下:

#include
#include
using namespace std;
int n,m,ans;
int f[5002][5002];   //描述生物之间吃与被吃关系的矩阵
int in[5002],out[5002]; 
int num[5002];  //到达i时的路径数 
queue q;
int main()
{
	cin>>n>>m;
	int x,y;
	for(int i = 0; i < m; i++)
	{
		cin>>x>>y;
		f[x][y] = 1;  //能量由x流向y 
		out[x]++;
		in[y]++;
	} 
	
	for(int i = 1; i <= n; i++)
	{
		if(in[i]==0)
		{
			num[i] = 1;
			q.push(i);        //入度为0,即生产者入队 
		} 
	}
	while(!q.empty())
	{
		int a = q.front();
		q.pop();
		for(int k = 1; k <= n; k++)
		{
			if(f[a][k]==0) continue;
			num[k] += num[a];     //更新
			num[k]%=80112002;
			in[k]--;        //食物少一个,入度减1 
			if(in[k]==0)    //入度为0才入队 
			{
				if(out[k]==0)    //出度为0的点为食物链终点,记录答案,不必入队 
				{
					ans += num[k];
					ans%=80112002;
					continue;
				}
				q.push(k);
			} 
			
		}
	 } 
	 
	cout<

洛谷P1048 采药(0-1背包)

采药
这是一个典型的 0-1背包问题 ,设草药数量为m,采药总时间为t,
定义一个二维数组 dp[ ][ ], dp[i][j] 表示把前 i 个物品装入容量为 j 的背包中获得的最大价值.
如果物品i的花费大于j,则不装物品i:

if(c[i] > j)
				dp[i][j] = dp[i-1][j];

如果物品i的花费小于等于j,则作比较:

dp[i][j] = max(dp[i-1][j],dp[i-1][j-c[i]] + v[i]);

完整代码如下:

#include
#include
using namespace std;
int t,m;
int c[105],v[105];      //采草药耗费的时间与草药的价值 
int dp[105][1005];

int ans()
{
	memset(dp,0,sizeof(dp));
	for(int i = 1; i <= m; i++)
	{
		for(int j = 0; j <= t; j++)
		{
			if(c[i] > j)
				dp[i][j] = dp[i-1][j];   //第i个物品消耗时间太长,返回到装前i-1个物品的状态
			else    //第i个物品可以装 
				dp[i][j] = max(dp[i-1][j],dp[i-1][j-c[i]] + v[i]); 
		}
	}
	
	return dp[m][t];
}
int main()
{
	cin>>t>>m;
	for(int i = 1; i <= m; i++)
	{
		cin>>c[i]>>v[i];
	}
	
	cout<

洛谷P1616 疯狂地采药(完全背包)

疯狂地采药
这题是上一题采药的进阶版,多了个条件: 每种草药可以不限数量地采,数据规模也变大了,有点麻烦.
第一感觉用贪心就可以了, 只要时间充足,一直采价值/时间比最大的草药就可以了,试一试.
仔细一思索,贪心肯定不行.比如总时间t=10,草药种类m=2,一种花费5价值5,一种花费6价值7,按照贪心,取后一种1株,总价值7,实际上最优采法为取两株第一种,总价值10.
学习了一下别人的方法,原来是一道完全背包的题目.
完全背包与0-1背包的区别:
完全背包是跟同一行的作比较,而0-1背包是与上一行作比较.

关键代码:

dp[j] = max(dp[j],dp[j-c[i]] + v[i]);

完整代码:

#include
using namespace std;
const int N = 1e4+5,M=1e7+5;
int t,m,c[N],v[N];
long long dp[M];
int main()
{
	cin>>t>>m;
	for(int i = 1;i <= m; i++)
		cin>>c[i]>>v[i];
	for(int i = 1; i <= m; i++)
		for(int j = c[i]; j <= t; j++)
			dp[j] = max(dp[j],dp[j-c[i]] + v[i]);
	cout<

洛谷P1802 五倍经验日(变异的0-1背包)

五倍经验日
n个人,x个药水,打赢每个人要花usei个药才能胜利,获得胜利经验wini;花费小于usei个药则失败,获得失败经验losei.求最大经验的5倍.
这题属于 变异的0-1背包问题.
思路:
药水够:

for(int j = x; j >= use[i] ; j--)
			dp[j] = max(dp[j]+lose[i],dp[j-use[i]] + win[i]);

药水不够:

for(int j = use[i]-1; j >= 0; j--)
			dp[j] += lose[i];

完整代码:

#include
using namespace std;
const int N = 1e3+5,M = 1e6+5;
int n,x;
long long dp[N];
int lose[M],win[M],use[N];
int main()
{
	cin>>n>>x;
	for(int i = 1; i <= n; i++)
		cin>>lose[i]>>win[i]>>use[i];
	for(int i = 1; i <= n; i++) 
	{
		for(int j = x; j >= use[i] ; j--)
			dp[j] = max(dp[j]+lose[i],dp[j-use[i]] + win[i]);
		for(int j = use[i]-1; j >= 0; j--)
			dp[j] += lose[i];
	}		
	cout<

洛谷P1002 过河卒(像青蛙跳台阶)

过河卒
一眼看过去有点像P4017最大食物链计数的那题,拓扑排序?
不用那么麻烦,想清楚了就很简单的一道题.

dp[i][j] 为从 (0,0) 点到 (i,j) 点的路径条数,因为点 (i,j) 只能由点 (i-1,j) 或者点 (i,j-1) 走过去,所以状态转移方程为:

dp[i][j] = dp[i-1][j] + dp[i][j-1];

当然 i=0 或者 j=0 的情况特殊考虑.

让我想起了 青蛙跳台阶 的题,都是从结果往前逆推, 只用考虑最后一步 就行了.

完整代码如下:

#include
using namespace std;
int x,y;       //记录B点坐标
int a,b;       //记录马的坐标 
int f[25][25];  //记录是否是马的控制点 
long long dp[25][25]; //记录路径数 

int dx[] = {0,-1,-2,-2,-1,1,2,2,1};  //马控制范围坐标偏移量 
int dy[] = {0,-2,-1,1,2,2,1,-1,-2};

int main()
{
	cin>>x>>y>>a>>b;
	for(int i = 0; i < 9; i++)   //标记马的控制点 
		if(a+dx[i]>=0 && a+dx[i]<=20 && b+dy[i]>=0 && b+dy[i]<=20)
			f[a+dx[i]][b+dy[i]] = 1;
	for(int i = 0; i <= x; i++)
		for(int j = 0; j <= y; j++)
		{
			if(f[i][j]) continue;
			else
			{
				if(i==0&&j==0)
					dp[i][j] = 1;
				else if(i==0&&j>0)
					dp[i][j] = dp[i][j-1];
				else if(i>0&&j==0)
					dp[i][j] = dp[i-1][j];
				else
					dp[i][j] = dp[i-1][j] + dp[i][j-1];
			}
		}
	cout<

你可能感兴趣的:(动态规划,动态规划)