动态规划刷题总结

刷题综述

这段时间刷了codeup上的动态规划专题,和一些PAT题库的题目,浙大的题库真的牛,两天就给我心态搞崩了,都有点想弃考。anyway,今天把之前codeup动规专题落下的一些题刷完了,一共大概有17题,涉及的主要题型有最大连续子序列和、最长不降子序列(LIS)、最长公共子序列(LCS)、最长回文子串、DAG最长回路和背包问题。不同的题有各自的特点,下面分别简述一下。

写在前面的阅读tips:本文给出的代码均来自于具体题目的片段,数组下标范围有时从0开始,有时从1开始,这是为了满足不同题目的需要而定,读者应避免产生混淆。有时概述部分使用的变量名与给出代码里的变量名不一致,但笔者认为给出的代码片段只是为了介绍核心思想,本身也没几行,应该不至于影响理解。

最大连续子序列和

给出一段序列arr,然你求出其和最大的连续子序列subarr,我们需要设置一个长度为序列元素个数n的dp数组,dp[i]存储的状态是以arr[i]结尾的连续子序列的最大和,当考虑dp[i]如何计算时,应当清楚这个连续子序列要么包含arr[i]的前一个元素,要么只有arr[i]自己,取舍的依据是dp[i-1]是否大于0,dp[i-1]大于0才能为子序列和的增长做出贡献,因此这时有:dp[i] = dp[i-1] + arr[i];而反之,dp[i] = arr[i]。
关键代码如下:

int ans = dp[0] = 0;
for(int i=1;i<n;i++){
			if(dp[i-1]<0){
				dp[i] = d[i];
			}else{
				dp[i] = dp[i-1] + d[i];
			}
			ans = (ans<dp[i])?dp[i]:ans;
		}

最长不降子序列(LIS)

这个问题会相对复杂一点,dp依然是长度为原序列arr元素个数n的数组,其dp[i]存储的状态是以arr[i]结尾的最长不降子序列的长度,注意这里并非是连续子序列,当考虑dp[i]如何计算时,需要遍历此前所有的序列元素,找到可以和arr[i]构成不降子序列的元素,并取dp值最大的与arr[i]结合,这就构成了更长的(多一个arr[i])的不降子序列,将其长度存储进dp[i]。
关键代码如下:

int ans = dp[0] = 1;
		//这里的状态转移,对于所有小于当前点的点集元素
		//当前点的dp值,等于其中dp最大的+1 
		for(int i=1;i<n;i++){
			dp[i] = 1;
			for(int j=0;j<i;j++){
				if(d[i]>d[j]&&dp[i]<dp[j]+1)
					dp[i] = dp[j] + 1;
			}
			ans = (ans<dp[i])?dp[i]:ans;
		}

最长公共子序列(LCS)

最长公共子序列涉及两个序列,暂设arr1、arr2,找出它们长度最长的公共子序列,这里的子序列是指元素相对位置一致,不要求连续。这个题目要建立二维的dp数组,每一维表示其中一个序列的下标范围,比如dp[i][j]存储的状态可定义为arr1下标范围到i的子序列和arr2下标范围到j的子序列,两者的最长公共子序列的长度,您可以明显感受到这样一来就做到把原问题的规模缩小了,其边界也很好得到:
当i为0时,arr1的子序列是空序列,因此所有第一维为0的dp值皆为0,因为空序列跟任意长度的序列所能得到的公共子序列最大长度皆是0;同理,所有第二维为0的dp值也为0。
那么如何给出状态转移方程呢?对于dp[i][j],它可与三个子问题有关:dp[i-1][j]、dp[i][j-1]和dp[i-1][j-1],当arr1[i]与arr2[j]相等,意味着它可以在dp[i-1][j-1]求得的公共子序列的基础上再增加一个元素,即可把dp[i][j]转移为dp[i-1][j-1]+1;当arr1[i]与arr2[j]不等时,可把dp[i][j]朝dp[i-1][j]和dp[i][j-1]两个方向上转化,去取其中较大者即可。
关键代码如下:

for (int i = 0;i < n;i++)
	dp[i][0] = 0;
for (int j = 0;j < m;j++)
	dp[0][j] = 0;
int ans = 0;
for (int i = 0;i < n;i++) {
	for (int j = 0;j < m;j++){
		if (arr1[i] == arr2[j])
			dp[i][j] = dp[i-1][j-1] + 1;
		else
			dp[i][j] = max (dp[i-1][j],dp[i][j-1]);
		}
		ans = (ans < dp[i][j]) ? dp[i][j] : ans;
	}

最长回文子串

这个问题要注意的是回文子串是原字符串中一个连续的子串,本问题的dp依然是一个二维数组,第一维可表示子串的起点,第二维可表示子串的终点,数组中存储的是起点到终点的范围中回文子串的最长长度。为了提高算法的效率,可以枚举回文子串的长度,关键代码如下:

//初始化,顺便判断一下L可否为2,注意若不可以不意味着不能达到更长的长度
for(int i=0;i<len;i++){
		dp[i][i] = 1;
		if(i<len-1){
			if(str[i]==str[i+1]){
				dp[i][i+1] = 1;
				ans = 2;
			}else{
				dp[i][i+1] = 0;
			}
		}
	}
	//求解dp数组
	for(int L=3;L<=len;L++){
		for(int i=0;i+L-1<len;i++){
			int j = i + L - 1;
			if(str[i]==str[j]){
				dp[i][j] = dp[i+1][j-1];
				if(dp[i][j])
					ans = max(L,ans);
			}else{
				dp[i][j] = 0;
			}
		}
	}

DAG最长回路

首先DAG指的是有向无环图,老朋友了,在之前的拓扑排序和AOE中我们处理的都是这种图。在AOE中我们基于拓扑排序求出了工程的关键路径,即最长的路径,而动态规划的方法可以让我们更加方便地求解此问题:不涉及拓扑排序,用递归的方法将状态向后继点转移,直到遇到出度为0的点即为边界,然后逐层返回。这里dp数组是一个以顶点数量为长度的一维数组,dp[i]存储的是以i号顶点为起点能够获得的最长路径长度,递归主体如下,以图的邻接表为例:

int DP(int u){
	if (dp[u]>0) return dp[u];
	for (int i=0;i<G[u].size();i++){
		int v = G[u][i].v;
		int w = G[u][i].w;
		if (dp[u]<DP(v)+w)
			dp[u] = dp[v] + w;
	}
	return dp[u];
}

因为dp初始化时全部设为0,意味着以一个点为起点至少能获得长度为0的路径,即只包含该点自身,所以dp值被更新就可以通过不为0这个条件来判断,直接返回,避免再次陷入递归。随后就是通过后继点更新该点dp值的过程,取所有后继中dp值加上边权最大的作为最终结果,求解完成后需要将该点dp值返回,注意对于图中出度为0,即没有后继点的点,它既不满足一开始的条件判断,也不能进入循环,所以将会直接返回其dp值,也就是0,而这就是递归边界。
动规不仅能解决DAG最长路问题,还能解决DAG最短路问题,并能解决确定终点、确定起点的DAG最长路和最短路问题。
对于确定终点的DAG最长路,将终点dp值初始化为0,对于其他出度为0的点,由于其一定不能够到达终点,所以设为-INF,这样在计算时也不用担心它对其路径上点dp值的更新而影响到问题的正确求解。此外,这个问题里需要设置vis数组,不像上一个问题,每个出度为0的点的更新都是有效的,这里只有来自终点的更新才是有效的,所以要剪去这些多余的路径,正所谓“人不能两次踏入同一条河流”,如何做到?设个标记即可,访问过的结点,对应vis数组里的值为true,反之为false,初始皆为false1。

int DP(int u){
	if (dp[u]>0) return dp[u];
	for (int i=0;i<G[u].size();i++){
		int v = G[u][i].v;
		int w = G[u][i].w;
		if (!vis[v]&&dp[u]<DP(v)+w)
			dp[u] = dp[v] + w;
	}
	vis[u] = true;
	return dp[u];
}

对于最短路和确定起点的最短路的求解,书上仅说其与最长路的求解思想是完全一致的,那么在求解框架不变的基础上,只要把取后继点最大值的步骤改为最小值即可。

0-1背包

给你一个固定容量的背包,还有一些具有体积、价值两种属性的物品,同一种物品具有相同的属性值,每种物品只能拿一个,问这固定容量的背包最多可以装下多大价值的物品。以背包的容积为一个维度的变量,以物品个数为另一个维度的变量,这样设出二维数组dp,其dp[i][j]的含义是,前i件物品装填容积为j的背包所能获得的最大价值,设计状态的目的,是为了能够有效地进行状态转移,将大问题进行正确的分割。如何转移dp[i][j],需要考虑第i件物品放不放进背包,若放进背包,能够获得的最大价值便是dp[i-1][j-cost[i]]+value[i],其中cost[i]是第i件物品占的空间,value[i]是第i件物品的价值;若不放,能获得的最大价值就是前i-1件物品在背包容积为j时取得的最大价值,即dp[i-1][j],最终dp[i][j]取两者中较大的值。

//边界为dp[0][v]=0,v从0到V枚举,表示没有物体放入价值为0
for (int i=1;i<=n;i++){
	for (int j=w[i];j<=V;j++){
		//状态转移方程/递推公式 
		dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+value[i]);
	}
} 

假如把物品件数看成一个个阶段,每个阶段的dp数组的值固然不同,但是这一个阶段的状态仅与上一个阶段的状态有关,最终我们是要求出物品件数为n时的dp值,所以前面所有状态是没必要一直保存的,可以想象到我们只需要一维的dp数组就可以完成运算,上一个阶段的dp数组已经求出,在求这一阶段时只需要从尾部开始逐个更新即可,因为您看状态转移时,我们需要用到上一个阶段同样背包容积或更小背包容积的状态值,所以需要从后往前更新。这就是滚动数组,适用于解决任何多阶段状态问题:

//滚动数组版,从后到前枚举,边界是i为0时,全部赋为0 
int dp[maxn];//初始化为全0即可 
for (int i=1;i<=n;i++){
	for (int j=V;j-w[i]>=0;j--){
		dp[j] = max(dp[j],dp[j-w[i]]+value[i]);
	}
} 

完全背包

与0-1背包有些微的差别,这里每种物品有无限个,对于每种物品你可以装任意多件,虽然问题似乎变得更加抽象了,但我们的dp数组含义没有发生变化,我们仅来考虑这个时候状态如何转移。计算dp[i][j],对于第i件物品,有装与不装两种选择,若装,状态将朝着dp[i][j-cost[i]]转移,不同于0-1背包朝着dp[i-1][j-cost[i]]转移,这是因为第i件物品可以放无穷多件,因此第二个维度不断减cost[i],直到无法容纳第i件物品为止;若不装,依然与0-1背包一样,朝着dp[i-1][j]转移。
二维数组版,更加直观一点:

for (int i=1;i<=n;i++){
	for (int j=0;j<=v;j++){
		if (j>=cost[i]){
			dp[i][j] = max(dp[i-1][j],dp[i][j-cost[i]]+value[i]);
		} else {
			dp[i][j] = dp[i-1][j];
		}
	}
}

完全背包同样是一个多阶段问题,要清楚的是这里状态转移是朝着本阶段背包容积更小的状态去转移的,所以必然要求我们是从头开始更新dp数组,滚动数组版:

for (int i=1;i<=n;i++){
	for (int j=w[i];j<=V;j++){
		dp[j] = max(dp[j],dp[j-w[i]]+value[i]);
	}
}

个例分析

上一个部分总结了几个我自己认为比较有代表性的题型,下面对其中某些题型拎一些有意思的题目分析一二。为了节省篇幅,每个题目只给出关键代码片段。

合唱队形

  • 题目描述
    N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学不交换位置就能排成合唱队形。
    合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1, 2, …, K,他们的身高分别为T1, T2, …, TK,
    则他们的身高满足T1 < T2 < … < Ti , Ti > Ti+1 > … > TK (1 <= i <= K)。
    你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
  • 输入
    输入的第一行是一个整数N(2 <= N <= 100),表示同学的总数。
    第一行有n个整数,用空格分隔,第i个整数Ti(130 <= Ti <= 230)是第i位同学的身高(厘米)。
  • 输出
    可能包括多组测试数据,对于每组数据,
    输出包括一行,这一行只包含一个整数,就是最少需要几位同学出列。

这个问题相当于求解最长单调子序列,可以看看上一个部分里我们已经讨论过这类题型,只需要正着求一组dp,再把原序列倒过来再求一组dp,两个dp数组里下标相加为n-1的对应着原序列同一个元素,这个元素就是子序列结尾的元素,故两个子序列正好可以拼成一个合唱队形。

fill(d1,d1+n,1);
fill(d2,d2+n,1);
	//求解d1数组
for (int i=1;i<n;i++){
	for (int j=0;j<i;j++){
		if (H[j]<H[i])
			d1[i] = max(d1[i],d1[j]+1);
	}
}
	//求解d2数组
reverse(H,H+n);
for (int i=0;i<n;i++){
	for (int j=0;j<i;j++){
		if (H[j]<H[i])
			d2[i] = max(d2[i],d2[j]+1);
	}
}
int k = 1;
for (int i=0;i<n;i++){
	k = max(k,d1[i]+d2[n-1-i]-1);
}
printf("%d\n",n-k); 

最大子矩阵

  • 题目描述
    已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵,你的任务是找到最大的非空(大小至少是1 * 1)子矩阵。
    比如,如下4 * 4的矩阵
    0 -2 -7 0
    9 2 -6 2
    -4 1 -4 1
    -1 8 0 -2
    的最大子矩阵是
    9 2
    -4 1
    -1 8
    这个子矩阵的大小是15。
  • 输入
    输入是一个N * N的矩阵。输入的第一行给出N (0 < N <= 100)。
    再后面的若干行中,依次(首先从左到右给出第一行的N个整数,再从左到右给出第二行的N个整数……)给出矩阵中的N2个整数,整数之间由空白字符分隔(空格或者空行)。
    已知矩阵中整数的范围都在[-127, 127]。
  • 输出
    测试数据可能有多组,对于每组测试数据,输出最大子矩阵的大小。

这个问题是比较复杂的,但是求解方法不可不谓精妙,可以参考这位博主的博客,我用自己的话概括一下,求一个子矩阵的和,必然要把它的所有行累加起来,这样相当于把二维矩阵变成了一个一维的序列,然后对这个序列进行累加求和。那么我们不妨当子矩阵的行数确定时(比如k),对原矩阵逐个累加k行,可以得到n-1-k个序列,对于每一个序列,便等同于求解其最大子序列和,所以这个问题最终可以这样被转化为最大连续子序列和问题,剩下的工作仅是对比求解最大值,这最大值就是子矩阵行数为k时能得到的最大大小,存在dp里,因此dp的索引值就是子矩阵的行数,其实本题的dp数组里存的状态相互之间不存在转移关系,本题的动规思想主要体现在求取每一个dp[k]的过程中。最终dp数组里的最大值就是本题的解:

void DP(int n){
	int** temp = new int*[n];
	for (int i=0;i<n;i++)
		temp[i] = new int[n];
	fill(dp,dp+n+1,-1*INF);
	int ans = dp[0];
	for (int k=1;k<=n;k++){
		//预处理,逐个累加原矩阵的k行,并把结果存为一个二维数组,返回这个数组的行数
		int l = predeal(temp,k,n);
		for (int i=0;i<l;i++){
			//求解dp[k] 
			dp[k] = max(dp[k],calLCM(temp[i],n));//这里的calLCM是我瞎取的名字,就是求最长连续子序列和
		}
		//求最大值 
		ans = max(ans,dp[k]);
	}
	printf("%d\n",ans);
	for (int i=0;i<n;i++)
		delete(temp[i]);
	delete(temp);//养成及时delete的好习惯
} 

矩形嵌套问题

  • 题目描述
    有n个矩形,每个矩形可以用a,b来描述,表示长和宽。矩形X(a,b)可以嵌套在矩形Y(c,d)中当且仅当a<c,b<d或者b<c,a<d(相当于旋转X90度)。例如(1,5)可以嵌套在(6,2)内,但不能嵌套在(3,4)中。你的任务是选出尽可能多的矩形排成一行,使得除最后一个外,每一个矩形都可以嵌套在下一个矩形内。
  • 输入
    第一行是一个正正数N(0<N<10),表示测试数据组数,
    每组测试数据的第一行是一个正正数n,表示该组测试数据中含有矩形的个数(n<=1000)
    随后的n行,每行有两个数a,b(0<a,b<100),表示矩形的长和宽
  • 输出
    每组测试数据都输出一个数,表示最多符合条件的矩形数目,每组输出占一行

这是一个用DAG最长回路思想求解的经典问题,当然对于完全没接触过的同学来说我认为还是比较难想到的,本题的key在于把每个矩形抽象为DAG图中的顶点,把矩形间的嵌套关系抽象为DAG图中的有向边,比如矩形A可以嵌套在矩形B里,就可以在A、B间建立一条有向边,每条边的权重都赋为1,求出最长回路的长度,代表最多可以获得这么多嵌套关系,可问题最后要求的是矩形的个数,加1即可。本题模块较多,那就附上完整的求解代码吧:

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 1010;
//矩形结构体,方便读入数据
struct rect{
	int a,b;
	rect(){
	}
	rect(int _a,int _b){
		a = _a;
		b = _b;
	}
}V[maxn]; //矩形结构体数组

vector<int> G[maxn];//DAG图的邻接表
int dp[maxn];//dp数组
int choice[maxn];//存储最长路某结点的后继点编号,为了能够输出路径,当然不是本题要求的,只是求着玩,不感兴趣的朋友跳过即可

void create(int n){//建立DAG的函数
	for(int i=0;i<n;i++)
		G[i].clear();
	for(int i=0;i<n-1;i++){
		for(int j=i+1;j<n;j++){
			rect A = V[i];
			rect B = V[j];
			//满足嵌套关系则建立有向边,一次性考虑双向
			if((A.a<B.a&&A.b<B.b)||(A.b<B.a&&A.a<B.b))
				G[i].push_back(j);
			else if((B.a<A.a&&B.b<A.b)||(B.b<A.a&&B.a<A.b))
				G[j].push_back(i);
		}
	}
}
int DP(int i){//递归求解最长路
	if(dp[i]>1) return dp[i];
	for(int j=0;j<G[i].size();j++){
		int temp = DP(G[i][j])+1;
		if(dp[i]<temp){
			dp[i] = temp;
			choice[i] = G[i][j];
		}
	}
	return dp[i];
}
void printPath(int k){//打印最长路,DFS算法
	if(choice[k]==-1)
		printf("(%d,%d)\n",V[k].a,V[k].b);
	else{
		printf("(%d,%d) ",V[k].a,V[k].b);
		printPath(choice[k]);
	}
}
void calDAG(int n){//整合前面的模块的主体求解函数
	create(n);
	for(int i=0;i<n;i++){//这个输出是调试的时候使用的,提交的时候要删去
		printf("(%d,%d):",V[i].a,V[i].b);
		for(int j=0;j<G[i].size();j++){
			int k = G[i][j];
			printf("(%d,%d) ",V[k].a,V[k].b);
		}
		printf("\n");
	}
	fill(dp,dp+n,1);//注意啦,这里dp初始化为1是为了计算路上的矩形数,单纯求最长路是初始化为0的哟
	fill(choice,choice+n,-1);
	for(int i=0;i<n;i++){
		//递归法求解dp数组
		dp[i] = DP(i);
	}
	int ans = 1,k;
	for(int i=0;i<n;i++){
		if(ans<dp[i]){
			ans = dp[i];
			k = i;
		}
	}
	if(ans==1) k = 0;
	printf("%d\n",ans);
	printPath(k);
}
int main(){
	int N,n;
	scanf("%d",&N);
	while(N--){
		scanf("%d",&n);
		for(int i=0;i<n;i++){
			scanf("%d %d",&(V[i].a),&(V[i].b));
		}
		calDAG(n);
	}
}

装箱问题

题目描述
【问题描述】
有一个箱子的容量为V(V为正整数,且满足0≤V≤20000),同时有n件物品(0的体积值为正整数。
要求从n件物品中,选取若干装入箱内,使箱子的剩余空间最小。
输入:1行整数,第1个数表示箱子的容量,第2个数表示有n件物品,后面n个数分别表示这n件
物品各自的体积。
输出:1个整数,表示箱子剩余空间。
【输入输出样例】
输入:
24 6 8 3 12 7 9 7
输出:
0

这个问题有意思的地方在于,它可以看成0-1背包问题,但这里物品的价值就是它的体积,想让箱子的剩余空间最小,可不就是让物品的总体积最大嘛。

//滚动数组求解
void DP(int n,int v){
	fill(dp,dp+v+1,0);//初始化
	//分阶段求解dp数组
	for (int i=1;i<=n;i++){
		for (int j=v;j>=V[i];j--){
			dp[j] = max(dp[j],dp[j-V[i]]+V[i]);
		}
	}
	int maxV = 0;
	for (int i=0;i<=v;i++){
		maxV = max(maxV,dp[i]);
	}
	//输出箱子剩余空间
	printf("%d\n",v-maxV);
}

货币系统

  • 题目描述
    母牛们不但创建了他们自己的政府而且选择了建立了自己的货币系统。
    [In their own rebellious way],,他们对货币的数值感到好奇。
    传统地,一个货币系统是由1,5,10,20 或 25,50, 和 100的单位面值组成的。
    母牛想知道有多少种不同的方法来用货币系统中的货币来构造一个确定的数值。
    举例来说, 使用一个货币系统 {1,2,5,10,…}产生 18单位面值的一些可能的方法是:18x1, 9x2, 8x2+2x1, 3x5+2+1,等等其它。
    写一个程序来计算有多少种方法用给定的货币系统来构造一定数量的面值。
    保证总数将会适合long long (C/C++) 和 Int64 (Free Pascal)。
  • 输入
    输入包含多组测试数据
    货币系统中货币的种类数目是 V 。 (1<= V<=25)
    要构造的数量钱是 N 。 (1<= N<=10,000)
    第 1 行: 二整数, V 和 N
    第 2 …V+1行: 可用的货币 V 个整数 (每行一个 每行没有其它的数)。
  • 输出
    单独的一行包含那个可能的构造的方案数。

这个问题在简单数学问题的模块里直接用循环来做的,但这样简单粗暴的做法对于这种比较复杂的问题来说显然是不适用的。这题也是背包问题的变形,且显然应该是完全背包问题,且因为是完全背包,最后一定能正好凑出给定面值,但不同于原来的思路,这里的价值该如何衡量呢?注意我们并不是在求最优化问题,而是统计所有的方案数,所以我们要做的是把子问题的解累加,而不是依据价值大小进行取舍。

void DP(long long v,int n){
	//给出要凑的面值和货币种数
	fill(dp,dp+v+1,0);
	dp[0] = 1;//凑面值为0的方案数总是1 
	for(int i=1;i<=n;i++){
		for(int j=curType[i];j<=v;j++){
			dp[j] = dp[j] + dp[j-curType[i]];
		}
	}
	printf("%lld\n",dp[v]); 
}

当然这里的面值可能非常大需要用long long型来存储,to be honest,个人认为在考试时遇到数据溢出这种问题还是很讨厌的,算法难道不应该侧重于思想吗,又不是在搞项目。

毕业bg

  • 题目描述
    每 年毕业的季节都会有大量毕业生发起狂欢,好朋友们相约吃散伙饭,网络上称为“bg”。参加不同团体的bg会有不同的感觉,我们可以用一个非负整数为每个 bg定义一个“快乐度”。现给定一个bg列表,上面列出每个bg的快乐度、持续长度、bg发起人的离校时间,请你安排一系列bg的时间使得自己可以获得最 大的快乐度。
    例如有4场bg:
    第1场快乐度为5,持续1小时,发起人必须在1小时后离开;
    第2场快乐度为10,持续2小时,发起人必须在3小时后离开;
    第3场快乐度为6,持续1小时,发起人必须在2小时后离开;
    第4场快乐度为3,持续1小时,发起人必须在1小时后离开。
    则获得最大快乐度的安排应该是:先开始第3场,获得快乐度6,在第1小时结束,发起人也来得及离开;再开始第2场,获得快乐度10,在第3小时结束,发起人正好来得及离开。此时已经无法再安排其他的bg,因为发起人都已经离开了学校。因此获得的最大快乐度为16。
    注意bg必须在发起人离开前结束,你不可以中途离开一场bg,也不可以中途加入一场bg。
    又因为你的人缘太好,可能有多达30个团体bg你,所以你需要写个程序来解决这个时间安排的问题。
  • 输入
    测试输入包含若干测试用例。每个测试用例的第1行包含一个整数N (<=30),随后有N行,每行给出一场bg的信息:
    h l t
    其中 h 是快乐度,l是持续时间(小时),t是发起人离校时间。数据保证l不大于t,因为若发起人必须在t小时后离开,bg必须在主人离开前结束。
    当N为负数时输入结束。
  • 输出
    每个测试用例的输出占一行,输出最大快乐度。

这道题题目本身就不太好理解,题中有一部分交代得不太清楚,如何理解发起人离开的时间,事实上,这题存在一个初始的时间参考点,假设为0,那么,对于所举例子中的4场bg,可以理解为:第一场发起人离校的时间点为1(时间以小时为单位记)、第二场发起人离校的时间点为3、第三场发起人离校的时间点为2、第四场发起人离校的时间点为1。所以,发起人离校时间其实是一个时间点。
这题依然是一个0-1背包问题的变形,但是要先对bg按发起人离校时间进行排序,这题的一些概念又与原始的0-1背包问题有一定差异,根本原因在于时间点与时间段两者之间的关系本来就比较暧昧:时间点的值等同于其从参考0点开始的时间段的值、时间点加上或减去时间段又可以得到另一个时间点的值。对于dp数组我们是从当前bg发起人的离校时间点开始更新,到等于当前bg持续时间段值表示的时间点结束,可能你已经被我绕晕了,直接附上代码片段直观感受一下:

sort(bg+1,bg+N+1,cmp);
int tmax = bg[N].t;
fill(dp,dp+tmax+1,0);
for (int i=1;i<=N;i++){
	for (int j=bg[i].t;j>=bg[i].l;j--){
		dp[j] = max(dp[j],dp[j-bg[i].l]+bg[i].h);
	}
}
int ans = *max_element(dp,dp+tmax+1);
printf("%d\n",ans);

放苹果

  • 题目描述
    把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。
  • 输入
    第一行是测试数据的数目t(0 <= t <= 20)。以下每行均包含二个整数M和N,以空格分开。1<=M,N<=10。
  • 输出
    对输入的每组数据M和N,用一行输出相应的K。

这题对我来说也算难题,如果从排列组合的方向上考虑就走偏了,动规不需要这么复杂,不需要瞻前顾后,它只用存储的历史状态来更新当前的状态,你要考虑的主体只有一个,那就是状态。假设dp[i][j]表示i个苹果放在j个盘子里的方案数,它可以怎么计算呢?如果所有盘子里都有苹果,那么每个盘子都去掉一个苹果与之便是等价的情形,这种情形下的方案数是dp[i-j][j],欸,出现了减法,万一i不够减怎么办?在我们设定的这个情况下i肯定是够减的,但总会出现i比j小的情况,这种情况下,一定会有j-i个盘子空出来,因此等同于dp[i][i]的情形;好的,让我们考虑至少有一个盘子里没有苹果的情况,这样把空盘子去掉得到的方案数是一样的,也就是可以转化为dp[i][j-1],所以有dp[i][j] = dp[i-j][j] + dp[i][j-1],本题用递归方法完成。

int DP(int m,int n){
	if(dp[m][n]!=0) return dp[m][n];//初始化为0,故不为0表示已更新
	if(n>m) return DP(m,m);//苹果树小于盘子数
	dp[m][n] = DP(m,n-1) + DP(m-n,n);//否则,上面分析的状态转移
	return dp[m][n];//返回计算的dp值
}

可以看到,本题的关键之一在于明确这样的事实:空盘子去掉不影响方案数的计算,这是第二维可以不断缩小的依据。而第一维得以缩小在于如果所有盘子都有了一个苹果,那么去掉这个共同特征也不会影响方案数的计算。这些都是我们通过分析盘子里放苹果的状态得到的状态转移依据,聚焦点其实非常小,但是问题得到了全局最优解,DP真的很恐怖。

反思总结

这篇博客是昨晚开始写的,也算万字长文了,主要就是对动规专题做个总结,免得以后时间一长又忘了。我目前只是一个算法初学者,DP对我来说虽然有趣又精妙,但有的时候也会因为状态实在难以构造而对它产生些许怨念,当被点拨一二后又立刻如鱼得水般写出流畅的代码,而再次折服于它的魅力。打个不太恰当的比喻吧,感觉玩DP就像打拳击,当解决规律成熟的题目时就像你在打专业赛,你面对的对手都是经过专业系统的培训的,你的比赛也是在确定的积分规则下进行的,你比赛时有规律可循、有信息可抓、有策略可把握;但当你遇到一个高度抽象的陌生问题,就如同你在野拳场比赛,你不知道比赛的尺度能有多大,也不知道对手能使出多少阴招,没有特定的规则约束你,也没有经验和信息给你参考,去打吧,能做到的最好的事情只是击碎自己的恐惧。
也许以后,随着学习程度的加深,动规也能够变成我称心如意的解题工具,那这篇文章就是一个青涩的起点,能够让我日后回顾时,看到身处旅程伊始的自己,曾怀着多大的热忱和希冀。且歌且行河滨路,落日晚风不足惜。

  • 21
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值