动态规划(Dynamic Programming, DP)初学

这几天对动态规划进行了一个初步的学习,现无聊进行一个看到目前为止的动态规划由易到难的总结。

首先是我自己所理解的动态规划原理:一切需要求最优的状态,都是由其前一个最优状态组合而来的,所以我们要求解最优问题,首先需要将问题分解,使得当前状态能够由前一个最优状态来表示。语言表述能力有限,直接上题目,体验能够更深。

爬楼梯问题

问题描述

假设你正在爬楼梯,需要n阶你才能到达楼顶,每次你只能爬1或2个台阶,你有多少种不同的方法可以爬到楼顶?

示例1:

输入:n=2

输出:2

解释:有两种方法可以爬到楼顶。

1. 1阶+1阶

2. 2阶

示例2:

输入:n=3

输出:3

解释:有三种方法可以爬到楼顶。

1. 1+1+1

2. 1+2

3. 2+1

分析:

题目要求n阶台阶到楼顶有几种方法,其实也可以看成是爬到n阶台阶有多少种方法。所以我们可以假设爬到n阶台阶有dp[n]种方法。而我们一次只能爬1阶或者2阶,所以当我们到达n阶台阶的时候,必然经过了n-1阶台阶或者n-2阶台阶这两个先前的状态,而到这两个状态也分别有其可爬的操作数量,所以有了dp[n]=dp[n-1]+dp[n-2]。以此思路,当我们从n=1开始算起,就可以推出最终我们所需要的n阶台阶爬上有多少种方法,时间复杂度为O(n)。

因为这个逻辑较为简单,所以不放代码示例,接下来讲的就是稍微有点难度的dp规划问题。

分苹果问题

问题描述:

一个老师手上有n个苹果,第i个苹果质量是w_{i},现在他想把这些苹果分给他的学生甲和乙。但是为了不让他们打架,根据质量决定尽量地分成两堆给他们。现在老师想知道每个人分得多少质量的苹果最合适。注:苹果不能劈开,并且如果不能正好均分,乙会拿到重的那一堆。

输入描述:

第一行输入一个整数n(2\leqslant n\leqslant 100),第二行n个整数,表示每个苹果的质量w_{i}(1\leqslant w_{i}\leqslant100)。

输出描述:

输出两个整数,分别表示甲和乙两个人得到的苹果质量。

示例:

输入:

3

2 2 2

输出:

2 4

输入:

3

2 5 8

输出:

7 9

分析:

首先我们最好的情况就是两个人能拿到一样重量的苹果,我们将n个苹果进行1至n的编号(不需要按照质量排列),以此假设m[n]为1到n个苹果的总质量。以dp[i]表示一个人得到的苹果质量,且i为一个人可得的最大质量(在本题中i即为m[n]/2)。由此我们将苹果编号从1开始遍历,计算每加进来一个苹果后对应的dp[i]为多少,最终分得的质量即为dp[i]=max(dp[i],dp[i-a[n]]+a[n]])

代码实现:

#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

const int N = 106;
int a[N], n, sum = 0;
int dp[10006];
int main(void)
{
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		cin >> a[i];
		sum += a[i];
	}
	for (int i = 0; i < n; i++)
	{
		for (int j = sum / 2; j >= a[i]; j--)
		{
			dp[j] = max(dp[j], dp[j - a[i]] + a[i]);
		}
	}
	cout << dp[sum / 2] << " " << sum - dp[sum / 2] << endl;
	return 0;
}

 这个是将苹果平均分给两个人,接下来介绍的问题就是将苹果分给多个人

分苹果问题2:

问题描述:

目前有m个苹果,n个盘子,每个盘子都可以放无数个苹果,但是每个盘子都不能是空。且不考虑盘子编号,即:当m=7,n=3,下面有三种分法是被认为是相同的。

1,1,5;5,1,1;1,5,1

问有多少种不同的分法。

输入格式

m,n

输出格式

一个整数,即不同的分法。

数据规模规定6<m\leqslant 200, 2\leqslant n\leqslant 6

样例输入:

输入:

7 3

输出:

4(四种分法分别为:1,1,5;1,2,4;1,3,3;2,2,3;)

分析:

可以将当前状态设置为dp[m][n],而这一状态的前一个状态有两种情况:

(1)第n个盘子本可以没有苹果,但是题目要求每个盘子都不能为空,所以第n个盘子始终放一个苹果(之后都保持一个),所以还剩下m-1个苹果分到n-1个盘子当中,即dp[m-1][n-1];

(2)每个盘子都不能为空,所以将每个盘子都先放置一个苹果,还剩下m-n个苹果分到n个盘子中,即dp[m-n][n];

所以最终的dp[m][n]=dp[m-1][n-1]+dp[m-n][n]。

同时当m<=n时dp[m][n]=1,当m>n才是正常的递推情况。

代码实现:

#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;

int m, n;
int dp[205][11];

int main()
{
	memset(dp, 0, sizeof(dp));
	cin >> m >> n;
	dp[0][0] = 1;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= m; j++)
		{
			if (i >= j) dp[i][j] = dp[i - j][j] + dp[i - 1][j - 1];
		}
	}
	cout << dp[m][n] << endl;
	return 0;
}

以上都是基础的分类规划,最后介绍的就是我学习DP的源头——蓝桥杯第十二届研究生组的真题,目前思路有点乱,但是介绍出来方便各位帮我看一下有什么解释方面的错误。

分果果问题:

问题描述:

小蓝要在自己的生日宴会上将n包糖果分给m个小朋友,每包糖果都要分出去,每个小朋友至少都要分得一包,也可以分得多包。

小蓝已经提前将糖果准备好了,为了在宴会当天能把糖果分的更平均一点,小蓝要先计算好分配方案。小蓝将糖果从1到n编号,第i包糖果重w_{i}。小朋友从1到m编号。每个小朋友只能分到编号连续的糖果。小蓝想了很久没想到合适的分配方案使得每个小朋友分到的糖果差不多重。因此需要你帮他一起想办法。为了更好的分配糖果,他可以再买一些糖果,让某一些编号的糖果有两份。当某个编号的糖果有两份时,一个小朋友最多只能分得其中的一份。

请找一个方案,使得小朋友分得的糖果的最大重量和最小重量的差最小,请输出这个差。

对所有评测用例,1\leqslant n\leqslant 1001\leqslant m\leqslant 501\leqslant w_{i}\leqslant 100。在评测数据中,w_{i}随机生成,在某个区间均匀分布。

输入格式:

输入第一行包含两个整数n和m,分别表示糖果包数和小朋友数量。

第二行包含n个整数w_{1},w_{2},···,w_{n},表示每包糖果的重量。

输出格式:

输出一个整数,表示在最优情况下小朋友分到的糖果的最大重量和最小重量的差。

样例输入1:

5 2

6 1 2 7 9

样例输出1:

0

样例输入2:

5 5

6 1 2 7 9

样例输出2:

2

分析:

题目求解的是两个人取得的糖果的差值最小是多少。我的思路就是先确定每个人取得的糖果最大值max_v,然后只需要考虑每个人尽量取到的糖果不超过max_v,然后差值最小的就可以分拣出来。

其中糖果的总重量设为total,由于每份糖果都可以买两份,所以分得最多和最小两个人的差出现的极限情况为——一个人完全没拿到糖果,另外m-1个人将糖果进行平分,由此得到max_v=total*2/(m-1),这个是max_v的最大值。

接着,我们就要设计一个dp状态:dp[i][j][k]表示第i个人取得的糖果的最后一个是j,第一个是k,且第i-1个人得到的糖果最后一个小于等于k,考虑到转移,我们假设kk是第i-2个人取到的最后一个糖果,那么就有dp[i][j][k]=max(dp[i][j][k],min(dp[i-1][k][kk],sum[j]-sum[kk])),其中sum[j]是糖果重量的前缀和。

由于一些糖果可以购买两次,而且每个人得到的糖果下标连续,我们可以发现第i个人可以得到的糖果一定是以(kk+1,k+1)中的某个下标为开头的,并且以j为结尾的,而我们最终分得糖果的结果肯定是以j结尾的,只要不超过max_v,我们对糖果能取多少取多少。因此最终输出的结果为min(maxv-dp[m][n][k])。

代码实现:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
using namespace std;

#define N 105

int n, m;
int w[N], sum[N], dp[N][N][N];
int ans = INT_MAX, maxw = 0, total = 0;

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		cin >> w[i];
		total += w[i];
		maxw = max(maxw, w[i]);
		sum[i] = sum[i - 1] + w[i];
	}
	if (m == 1)
	{
		printf("0\n");
		return 0;
	}
	int Max_ave = total * 2 / (m - 1);
	maxw = max(maxw, Max_ave / 2);
	for (int max_v = Max_ave; max_v >= maxw; max_v--)
	{
		memset(dp, 0, sizeof(dp));
		dp[0][0][0] = max_v;
		for (int i = 1; i <= m; i++)
		{
			for (int j = 1; j <= n; j++)
			{
				for (int k = 0; k <= j; k++)
				{
					if (k) dp[i][j][k] = dp[i][j][k - 1];
					for (int kk = k; kk >= 0; kk--)
					{
						if (sum[j] - sum[kk] <= max_v)
						{
							dp[i][j][k] = max(dp[i][j][k], min(dp[i - 1][k][kk], sum[j] - sum[kk]));
							if (i == m && j == n)ans = min(ans, max_v - dp[i][j][k]);
						}
					}
				}
			}
		}
	}

	printf("%d\n", ans);
	return 0;
}

由于最后一个思考太过复杂,参考了【蓝桥杯 分果果】_三生河畔的博客-CSDN博客_分果果 蓝桥杯

但是对其中的代码进行了重新解读和规范。

由此,感觉我的DP应该算是入门了吧=。=

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值