20190501 DP总结

这里是苦逼熬夜 认真勤奋的某对前段时间学习的动态规划的总结。

首先,要对动规有一个基本的认识。

动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法,所以我们亲切地称它为DP,也叫对拍

动态规划就是在著名的最优化原理(principle of optimality)的基础上,把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,来解决过程优化问题的方法。

ok,上面的可以看作废话,下面讲的是动规的基础知识。

动态规划问题中的术语

阶段:

把所给求解问题的过程恰当的分成若干互相联系的阶段,以便于求解。一般阶段就是递归的最外侧循环,过程不同,阶段数就可能不同.描述阶段的变量称为阶段变量。

状态:

表示每个阶段中面临的自然状况或客观条件,是不可控制因素。过程的状态通常可以用一个或一组数来描述,称为状态变量。

决策:

一个阶段的状态给定以后,从该状态演变到下一阶段某个状态的一种选择(行动)称为决策。描述决策的变量称决策变量,因状态满足无后效性,故在每个阶段选择决策时只需考虑当前的状态而无须考虑过程的历史。

状态阶段决策是构成动态规划算法的三要素。

状态转移:

从一个阶段的一个阶段转移到下一阶段的某个状态的一种选择行为,叫做状态转移。

无后效性:

即在此后过程的演变中不再受到此前的各种状态以及决策的影响,简单地说,就是“未来与过去无关”,当前的状态是此前历史的一个完整总结,此前的历史只能通过当前的状态去影响过程未来的演变。

策略:

由每个阶段的决策组成的序列称为策略。

最优化原理和最优子结构:

一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。

子问题的重叠性:

动态规划实际上就是搜索的优化,将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。

动规适用的基本条件:

必须满足子问题重叠性,最优子结构性质和无后效性。

动规问题一般解题步骤:

1.判断问题是否具有最优子结构性质,若不具备则不能使用动规。

2.把问题分成若干个子问题(分阶段)。

3.建立状态转移方程(递推公式)。

4.找出边界条件。

5.将已知边界值代入方程。

6.递推求解。

(以上内容纯属虚构 借鉴自百度百科和课本)

然后,介绍动规的几类问题。

动态规划一般可分为 线性背包双进程区间平面树形 几类。

(一)线性动规(入门)

概念:

具有线性阶段划分的动态规划算法统称为线性DP。

典型题:

如:LIS,LCS,数字三角形等。

1.数字三角形

题目描述

观察下面的数字金字塔。

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

        7 
      3   8 
    8   1   0    
  2   7   4   4     
4   5   2   6   5

在上面的样例中,从7 到 3 到 8 到 7 到 5 的路径产生了最大

输入格式

第一个行包含 R (1<=R<=1000) ,表示行的数目。

后面每行为这个数字金字塔特定行包含的整数。

所有的被供应的整数是非负的且不大于100。

输出格式

单独的一行,包含那个可能得到的最大的和。

样例数据

input

5

7

3 8

8 1 0

2 7 4 4

4 5 2 6 5

output

30

数据规模与约定

时间限制:1s

空间限制:256MB

这题用深搜可以直接模拟,但很显然是会超时的。所以我们可以用记忆化搜索,记录每个点到底部的最优路径。

但这里要讲的是用递推的办法。

从底部开始,记录到从底部到a[i,j]的最大值,最后得到的a[1,1]就是答案。

下面是代码:

#include 
using namespace std;
long long n, a[1001][1001] = {}; 
int main()
{
	freopen("numtri.in", "r", stdin);
	freopen("numtri.out", "w", stdout);
	cin >> n;
	for (int i = 1; i <= n; i++)  
	for (int j = 1; j <= i; j++)  
	cin >> a[i][j];
	for (int i = n - 1; i >= 1; i--)
	for (int j = 1; j <= i; j++)
	a[i][j] = a[i][j] + max(a[i + 1][j], a[i + 1][j + 1]);
	cout << a[1][1] << endl;
	return 0;
}

2.导弹拦截

题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭,由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹的枚数和导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,每个数据之间有一个空格),计算这套系统最多能拦截多少导弹?

如果要拦截所有导弹最少要配备多少套这种导弹拦截系统?

输入格式

第一行数字n表示n个导弹(n<=200) 第二行n个数字,表示n个导弹的高度

输出格式

一个整数,表示最多能拦截的导弹数

一个整数,表示要拦截所有导弹最少要配备的系统数

样例数据

input

8
389 207 155 300 299 170 158 65

output

6
2

数据规模与约定

时间限制:1s
空间限制:256MB

第一个问题其实就是求最长不上升子序列的长度。

第二个问题涉及贪心。

第i个导弹在1~i-1个导弹中找到比自己高的最低的一个导弹,表示两个导弹用一套系统,f[i]表示到i为止某套系统拦截的导弹的最大个数,输出f中最大值,即为最长不上升子序列的长度,再用贪心算法得到第二问的解。

下面是代码:

#include
using namespace std;
int n;
int a[210],f[210],b[210];
int main()
{
	freopen("missile.in","r",stdin);
	freopen("missile.out","w",stdout);
	cin>>n;
	for (int i=1;i<=n;i++)
	cin>>a[i]; 
	memset(f,0,sizeof(f));
	memset(b,0,sizeof(b));
	int maxx1=0,maxx2=0;
	for (int i=1;i<=n;i++)
	{
		for (int j=1;jf[i]) f[i]=f[j];
		f[i]++;
		if (f[i]>maxx1) maxx1=f[i];
		int x=0;
		for (int j=1;j<=maxx2;j++)
		if (b[j]>=a[i]) if (x==0) x=j;
		                else if (b[j]

(二)资源分配类

这类问题就是要把一些资源分给一些公司(人),不同的分配方法会带来不同的效益,求解怎样分配最优。

1.机器分配

题目描述

某总公司拥有高效生产设备M台,准备分给下属的N个分公司。各分公司若获得这些设备,可以为总公司提供一定的盈利。问:如何分配这M台设备才能使公司得到的盈利最大?求出最大盈利值。 分配原则:每个公司有权获得任意数目的设备,但总台数不得超过总设备数M。其中M<=100,N<=100。

输入格式

第一行为两个整数M,N。接下来是一个N×M的矩阵,其中矩阵的第i行的第j列的数Aij表明第i个公司分配j台机器的盈利。所有数据之间用一个空格分隔。

输出格式

只有一个数据,为总公司分配这M台设备所获得的最大盈利。

样例数据

input

3 2
1 2 3
2 3 4

output

4

数据规模与约定

时间限制:1s
空间限制:256MB

f[i,j]表示i个公司总共分配j个机器的最大盈利。

那么我们只需要知道第i个公司分了几台机器,便可以根据前i-1个公司来推出。

由此可以得出状态转移方程:

f[i][j] = max{f[i - 1][k], a[i][j - k]} (0<=k<=j)

下面是代码:

#include 
using namespace std;
int n, m, a[110][110], f[110][110];
int main()
{   
    freopen("allot.in", "r", stdin);
    freopen("allot.out", "w", stdout);
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; i++)
	for (int j = 1; j <= n; j++)
	{
	    scanf("%d", &a[i][j]);
	    f[i][0] = 0;
    }
    for (int i = 1; i <= m; i++)
    for (int j = 0; j <= n; j++)
    for (int k = 0; k <= j; k++)
	f[i][j] = max(f[i][j], f[i - 1][k] + a[i][j - k]);
    printf("%d", f[m][n]);
    return 0;
} 

2.复制书稿

题目描述

现在要把m本有顺序的书分给k个人复制(抄写),每一个人的抄写速度都一样,一本书不允许给两个(或以上)的人抄写,分给每一个人的书,必须是连续的,比如不能把第一、第三、第四本书给同一个人抄写。 现在请你设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间。

输入格式

第一行两个整数m,k;(k≤m≤500) 第二行m个整数,第i个整数表示第i本书的页数

输出格式

共k行,每行两个整数,第i行表示第i个人抄写的书的起始编号和终止编号。k行的起始编号应该从小到大排列,如果有多解,则尽可能让前面的人少抄写。

样例数据

input

9 3
1 2 3 4 5 6 7 8 9

output

1 5
6 7
8 9

数据规模与约定

时间限制:1s
空间限制:256MB

用 f[i, j] 表示前 i 个人抄写前 j 本书所用的最长时间,那么我们可以令第 i 个人抄第 k + 1 本书到 j 本书的总页数,那么便可以由前 i - 1 个人抄 1 – k 本书来进行转移。

于是得到状态转移方程:

f[i][j]=min(max(f[i-1][k],w[k+1,j])(0 <= k <= j)

用w[k + 1, j] 表示从第 k + 1 本书到第 j 本书的总页数(当然,也可以用sum[i]表示第1~i本书的总页数,w[k + 1, j] = sum[j] - sum[k])。

因为要输出方案,并且要保证前面的人少抄一些,所以我们可以用贪心,让后面的人多抄一些。

下面是代码:

#include 
using namespace std;
int m, k, a[520], sum[520], f[520][520];
void print(int i, int j)
{
	if (i == 0) return;
	if (i == 1)
	{
		cout << 1 << " " << i << endl;
		return;
	}
	else 
	{
		int temp = i, x = a[i];
		while (x + a[temp - 1] <= f[k][m] && temp > 1)
		x += a[--temp];
		print(temp - 1, j - 1);
		cout << temp << " " << i << endl;
	}
}
int main()
{
	freopen("input.in", "r", stdin);
	freopen("output.out", "w", stdout);
	memset(f, 10, sizeof(f)); 
	cin >> m >> k;
	for (int i = 1; i <= m; i++)
	{
		cin >> a[i];
		sum[i] = sum[i - 1] + a[i];
		f[1][i] = sum[i];
	}
	for (int i = 2; i <= m; i++)
	for (int j = 1; j <= m; j++)
	for (int l = 1; l < j; l++)
	f[i][j] = min(f[i][j], max(f[i - 1][l], sum[j] - sum[l]));
	print(m, k);
	return 0;
}

总结:

资源分配类一般可以用两维解决,一维是接受物品的单位,另一维是要分配的事物。

这类动规在时间上不一定超时,但可能会超空间,所以我们可以用两个一维数组分别记录当前阶段的状态和上一阶段的状态,用这种滚动数组的方法进行空间上的优化。

(三)背包问题

可以认为是资源分配的一个分支,单独将其拿出来,是因为一篇超有名气(?)的文章——《背包九讲》。

1.0/1背包

问题描述

有n件物品和一个容量为C的背包。第i件物品的重量为w[i],价值为v[i]。

求解将那些物品装入背包可使价值总和最大。

1) 二维数组表示

用f[i, c]表示前 i 件物品恰放入一个容量为 c 的背包可以获得的最大价值。

状态转移方程:
f [ i ] [ c ] = m a x { f [ i − 1 ] [ c ] (不选这件物品) f [ i − 1 ] [ c − w [ i ] ] + v [ i ] (选择这件物品) f[i][c]=max \begin{cases} f[i−1][c]& \text{(不选这件物品)}\\ f[i−1][c−w[i]] + v[i]& \text{(选择这件物品)} \end{cases} f[i][c]=max{f[i1][c]f[i1][cw[i]]+v[i](不选这件物品)(选择这件物品)

这样做的核心代码如下:

//注意边界处理
for (int i = 1; i <= n; i++)
for (int c = 0; c <= C; c++)
{
    f[i][c] = f[i - 1][c];
    if (c >= w[i]) f[i][c] = max(f[i][c], f[i - 1][c - w[i]] + v[i]);
}

时间复杂度,空间复杂度:都是O(NC)。

**2)**一维数组表示

因为f[i][c]只与f[i-1]层有关系,所以我们可以将 i 这一维优化掉。

那么,第 i 层的f[c] 只与第 i-1 层的f[c] 和 f[c-w[i]]有关。

因为f[c-w[i]] 在 f[c] 的左边,所以我们在求f[c] 的时候,必须保证f[c-w[i]] 还是第 i-1层的最优值,所以在递推时,c 要从 C 开始,倒着推,这样才能保证f[c] 左边的状态没有被第 i个物品更新过,还是第 i-1层的状态。

核心代码如下:

for (int i = 1; i <= n; i++)
for (int c = C; c >= 0; c--)
if (c >= w[i]) f[c] = max(f[c], [c - w[i]] + v[i]);

时间复杂度:O(NC)

空间复杂度:O(C)

3) 一维之下的一个常数优化

没有必要让循环的下限为0。

代码如下:

int bound, sumw = 0;
for (int i = 1; i <= n; i++)
{
    sumw += w[i];
    bound = max(c - sumw, w[i]);
    for (int c = C; c >= bound; c--)
    if (c >= w[i]) f[c] = max(f[c], f[c - w[i]] + v[i]);
}

4) 初始化时的细节
要求恰好装满,则初始化时令 f[0] = 0,其他的 f[i] = -INF,这样就可以知道是否有解了。

如果不用恰好,那么应该让 f 所有元素都置0。

2.完全背包

问题描述

有n 物品和一个容量为C的背包,第i 物品的重量是w[i],价值是v[i],数量无限。求解将哪些物品装入背包可使价格总和最大。

1) 基本算法:将其转换为0/1背包

这个问题非常类似于0/1 背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0 件、取1 件、取2件……等很多种。

仍按照0/1背包的思路,令f[i, c]表示前 i 种物品恰放入一个容量为 c 的背包的最大价值。

得到状态转移方程为:

f[i][c] = max{f[i - 1][c - k * w[i]] + k * v[i]} (0 <= k*w[i] <= c)

两个优化:

1.如果物品i, j 满足w[i] <= w[j] 且 v[i] >= v[j] , 就可以把物品j 去掉。

2.现将重量大于C的物品去掉,然后用类似桶排序或计数排序的方法,计算出费用相同的物品中那个价值更高。

2) 更优的算法

for (int i = 1; i <= n; i++)
for (int c = 0; c <= C; c++)
if (c >= w[i]) f[c] = max(f[c], f[c - w[i]] + v[i]);

为什么只是把0/1背包的循环顺序反一下就可以做完全背包呢?

0/1背包逆推是为了让物品只使用一次,而正推,f[c - w[i]] 的值已经被第 i 个物品更新过了,所以意味着第 i 个物品可以在需要的时候随便使用,即不限制个数。

3.多重背包

问题描述

有n种物品和一个容量作为C的背包,第i种物品的重量为w[i],价格为v[i],数量为a[i],求解将哪些物品装入背包可使价值总和最大。

1) 直接拆分法

就是把每件物品分开,把第 i 种物品看成 a[i] 个独立的个体,用0/1背包做,但这样做的效率很低。

于是,这里隆重推出一种方法——二进制法

2) 二进制法

众所周知,从20,21,22,⋅⋅⋅,2k−1 ,这k个2的整数次幂中选出若干个相加能表示出0 ~ 2k−1之间的任何整数。

于是,我们可以把数量为c[i]的第i种物品拆成p + 2 (p表示满足20+21+⋅⋅⋅+2p<=c[i]的最大整数,r[i]=c[i]−20−21−⋅⋅⋅−2p)个物品,它们的体积分别为:

20∗v[i], 21∗v[i], ⋅⋅⋅, 2p∗v[i], r[i]∗v[i]

这 p+2 个物品就可以凑成0~c[i]∗v[i] 间所有能被v[i]整除的数,并且不能凑成大于c[i]∗v[i]的数 ,等价于原题中体积为 v[i] 的物品可以使用0 ~c[i]次。这种方法仅把每种物品拆成了O(log c[i])个,效率较高。

例题 逃亡的准备

题目描述

在《Harry Potter and the Deathly Hallows》中,Harry Potter他们一起逃亡,现在有许多的东西要放到赫敏的包里面,但是包的大小有限,所以我们只能够在里面放入非常重要的物品,现在给出该种物品的数量、体积、价值的数值,希望你能够算出怎样能使背包的价值最大的组合方式,并且输出这个数值,赫敏会非常地感谢你。

输入格式

(1)第一行有2个整数,物品种数n和背包装载体积v。
(2)2行到n+1行每行3个整数,为第i种物品的数量m、体积w、价值s。.

输出格式

仅包含一个整数,即为能拿到的最大的物品价值总和。

样例数据

input

2 10
3 4 3

2 2 5

output

13

【注释】 选第一种一个,第二种两个。 结果为31+52=13

数据规模与约定

对于100%的数据 1<=v<=5000 1<=n<=5000 1<=m<=5000 1<=w<=5000 1<=s<=5000

时间限制:1s
空间限制:256MB

这是一道模板题,用二进制法做。

下面是代码:

#include
using namespace std;
int n, C, w[5100], a[5100], v[5100], f[5100]; 
int main()
{ 
    freopen("cx.in", "r", stdin);
    freopen("cx.out", "w", stdout);
    scanf("%d%d", &n, &C);
    for (int i = 1; i <= n; i++)
    scanf("%d%d%d", &a[i], &w[i], &v[i]);
    for (int i = 1; i <= n; i++)
    if (a[i] * w[i] > C)
    {
	    for (int c = w[i]; c <= C; c++)
	    f[c] = max(f[c], f[c - w[i]] + v[i]);
	}
	else
	{
	 	int k = 1, ans = a[i];
	 	while (k < ans)
	 	{ 
	 		for (int c = C; c >= k * w[i]; c--)
	 	    f[c] = max(f[c], f[c - k * w[i]] + k * v[i]);
	 		ans -= k; 
	 		k += k;
	 	}
	    for (int c=C;c>=ans;c--)
		f[c] = max(f[c], f[c - ans * w[i]] + ans * v[i]); 
	 }
	printf("%d", f[C]);	
}

4.混合背包问题

问题描述

还然是背包问题,不过有的物品只能取一次(0/1背包),有的物品却能取无限次(完全背包),有的物品能取有限次(多重背包)。

同样是按物品划分阶段。

下面是伪代码:

for (int i = 1; i < N; i++)
{
    if (物品i属于0/1背包)
    {
        //按照0/1背包做法取物品i
    }
    else if (物品i属于完全背包)
    {
        //按照完全背包做法取物品i
    }
    else if (物品i属于多重背包)
    {
        //按照多重背包做法取物品i
    }
}

5.二维费用背包

问题描述

有n 件物品和一个容量为C、容积为U 的背包。第i件物品的重量是w[i] ,体积是u[i],价值是v[i] 。

求解将哪些物品装入背包可使价值总和最大

(1) 0/1背包的表示方法

费用加了一维,只需把状态也加一维。

1. 状态表示:设f[i][c][u]为前i件物品付出两种代价分别为c 和u 时可以获得的最大价值。

2. 状态转移方程:
f [ i ] [ c ] [ u ] = m a x { f [ i − 1 ] [ c ] [ u ] f [ i − 1 ] [ c − w [ i ] ] [ u − u [ i ] ] + v [ i ] f[i][c][u]=max \begin{cases} f[i−1][c][u]& \text{}\\ f[i-1][c-w[i]][u-u[i]]+v[i]& \text{} \end{cases} f[i][c][u]=max{f[i1][c][u]f[i1][cw[i]][uu[i]]+v[i]

当然,为了节省空间,可以把i去掉。

3. 一个启示:当发现由熟悉的动态规划题目变形而来的题目时,在原来的状态中加一维以满足新的限制,这是一种比较通用的方法。

(2) 限制物品总个数的0/1背包

有n 件物品和一个容量为C 的背包。第i ii件物品的重量是w[i] ,价值是v[i]。

现在要求装入背包的物品个数不超过M 。求解将哪些物品装入背包可使价值总和最大。

其实,把个数看成容积,加一个限制条件就行了。

(3) 二维费用的完全背包和多重背包问题

循环时仍然按照完全背包(顺序循环)和多重背包(分割)的方法操作,只不过比完全背包和多重背包多了一维。

6.分组背包

问题描述

有n物品和一个容量为C的背包。第i件物品的重量是w[i],价值是v[i]。

这些物品被划分为K组,每组中的物品互相冲突,最多选一件。

求解将哪些物品装入背包可使价值总和最大。

1. 状态表示:f[k][c]表示前 k 物品花费 c 代价时可获得的最大价值。

2. 状态方程转移:

f [ k ] [ c ] = m a x { f [ k − 1 ] [ c ] f [ k − 1 ] [ c − w [ i ] ] + v [ i ] 物品i属于第k组 f[k][c]=max \begin{cases} f[k−1][c]& \text{}\\ f[k-1][c-w[i]]+v[i]& \text{物品i属于第k组} \end{cases} f[k][c]=max{f[k1][c]f[k1][cw[i]]+v[i]物品i属于第k

以下是伪代码:

for (int k = 1; K <= K; K++)
for (int c = C; c >= 0; c--)
for (所有属于第k组的物品)
if (c >= w[i]) f[c] = max(f[c], f[c - w[i] + v[i]);

要注意的是这里的循环顺序不能改变。

7.有依赖的背包问题

例题 金明的预算方案

问题描述

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“ 你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 N元钱就行 ”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

主件 附件
电脑 打印机,扫描仪
书柜 图书
书桌 台灯,文具
工作椅

如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有0 个、1个或 2 个附件。附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的 N 元。于是,他把每件物品规定了一个重要度,分为 5 等:用整数 1~5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 元的整数倍)。他希望在不超过 N 元(可以等于N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第 j 件物品的价格为 v[j],重要度为 w[j]共选中了 k 件物品,编号依次为 j1,j2,……,jk,则所求的总和为:

v[j1]×w[j1]+v[j2]×w[j2]+⋯+v[jk]×w[jk]

请你帮助金明设计一个满足要求的购物单。

【输入文件】

输入文件 budget.in 的第 1 行,为两个正整数,用一个空格隔开:Nm(其中 N(<32000)表示总钱数,m(<60)为希望购买物品的个数)

从第 2 行到第 m+1 行,第 j行给出了编号为 j−1 的物品的基本数据,每行有 3 个非负整数 v p q(其中 v 表示该物品的价格(v<10000),p 表示该物品的重要度(1~5),q 表示该物品是主件还是附件。如果 q=0,表示该物品为主件,如果 q>0,表示该物品为附件,q 是所属主件的编号)

【输出文件】

输出文件 budget.out 只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)。

【样例 1】

ex_budget1.in

1000 5

800 2 0

400 5 1

300 5 1

400 3 0

500 2 0

ex_budget1.ans

2200

时间限制:1s

空间限制:10MB

只考虑主件,所以依旧是f[i][j](也可以使用一维数组),先做主件的0/1背包,在枚举当前主件空间时,再做一次基于附件的0/1背包。

以下为代码:

#include 
using namespace std;
int n, m;
int a[33000], f[33000] = {};
int v[70], q[70], w[70];
int main()
{   
	freopen("budget.in", "r", stdin);
    freopen("budget.out", "w", stdout);
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; i++)
	{
	  	scanf("%d%d%d", &v[i], &w[i], &q[i]);
	  	w[i] = w[i] * v[i];
	}
	for (int i = 1; i <= m; i++)
	{
	  	if (q[i] == 0) 
	  	{  
		  	for (int j = 1; j < v[i]; j++) a[j] = 0;
		 	for (int j = v[i]; j <= n; j++) a[j] = f[j - v[i]] + w[i];
		 	for (int j = 1; j <= m; j++)
		  	if (q[j] == i)
		   	{
		   	 	for (int k = n; k >= v[i] + v[j]; k--)
		   	   	a[k] = max(a[k], a[k - v[j]] + w[j]);
		   	}
			for (int j = v[i]; j <= n; j++)
		   	f[j] = max(a[j], f[j]); 
	  	}	
	} 
	printf("%d", f[n]);
	return 0; 
}

(四)双进程类

就是有两个同时进行的决策,它们是互相影响的。

1.最长公共子序列

题目描述

字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。 令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列,使得对所有的j=0,1,…,k-1,有xij = yj。

例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。

对给定的两个字符序列,求出他们最长的公共子序列。

输入格式

第1行为第1个字符序列,都是大写字母组成,以”.”结束。长度小于5000。 第2行为第2个字符序列,都是大写字母组成,以”.”结束,长度小于5000。

输出格式

输出上述两个最长公共子序列的长度。

样例数据

input

ABCBDAB.

BACBBD.

output

4

数据规模与约定i

时间限制:1s
空间限制:256MB

我们可以用f[i,j]记录序列X的前 i 位和序列Y的前 j 位为最长公共子序列的长度。

递推公式如下:

f [ i ] [ j ] = { 0 i = 0 或 j = 0 m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] ) i, j>0, xi != yi f [ i − 1 ] [ j − 1 ] + 1 i, j>0, xi == yi f[i][j]= \begin{cases} 0& \text{i = 0 或 j = 0}\\ max(f[i - 1][j],f[i][j-1])& \text{i, j>0, xi != yi}\\ f[i - 1][j-1]+1& \text{i, j>0, xi == yi} \end{cases} f[i][j]=0max(f[i1][j],f[i][j1])f[i1][j1]+1i = 0  j = 0i, j>0, xi != yii, j>0, xi == yi

代码如下:

#include 
using namespace std;
string a, b;
int f[5001][5001] = {};
int main()
{
	freopen("lcs.in", "r", stdin);
	freopen("lcs.out", "w", stdout);
    cin >> a >> b;
	a.erase(a.end() - 1);
    b.erase(b.end() - 1);
    for (int i = 0; i < a.size(); i++)
    for (int j = 0; j < b.size(); j++)
    {
        f[i + 1][j + 1] = max(f[i][j + 1], f[i + 1][j]);
        if (a[i] == b[j]) f[i + 1][j + 1] = max(f[i + 1][j + 1], f[i][j] + 1);
    }
    cout << f[a.size()][b.size()] << endl;
	return 0;
}

2.配置魔药

题目描述

在《Harry Potter and the Chamber of Secrets》中,Ron的魔杖因为坐他老爸的Flying Car撞到了打人柳,不幸被打断了,从此之后,他的魔杖的魔力就大大减少,甚至没办法执行他施的魔咒,这为Ron带来了不少的烦恼。

这天上魔药课,Snape要他们每人配置一种魔药(不一定是一样的),Ron因为魔杖的问题,不能完成这个任务,他请Harry在魔药课上(自然是躲过了Snape的检查)帮他配置。 现在Harry面前有两个坩埚,有许多种药材要放进坩埚里,但坩埚的能力有限,无法同时配置所有的药材。一个坩埚相同时间内只能加工一种药材,但是不一定每一种药材都要加进坩埚里。加工每种药材都有必须在一个起始时间和结束时间内完成(起始时间所在的那一刻和结束时间所在的那一刻也算在完成时间内),每种药材都有一个加工后的药效。

现在要求的就是Harry可以得到最大的药效。

输入格式

输入文件的第一行有2个整数,一节魔药课的t(1≤t<≤500)和药材数n(1≤n≤100)。

输入文件第2行到n+1行中每行有3个数字,分别为加工第i种药材的起始时间t1、结束时间t2、(1≤t1≤t2≤t)和药效w(1≤w≤100)。

输出格式

只有一行,只输出一个正整数,即为最大药效。

样例数据

input

7 4

1 2 10

4 7 20

1 3 2

3 7 3

output

35

【注释】本题的样例是这样实现的:第一个坩埚放第1、4种药材,第二个坩埚放第2、3种药材。这样最大的药效就为10+20+2+3=35。

数据规模与约定

对于30%的数据 1<=t<=500 1<=n<=15 1<=w<=100 1<=t1<=t2<=t

对于100%的数据 1<=t<=500 1<=n<=100 1<=w<=100 1<=t1<=t2<=t

时间限制:1s
空间限制:256MB

先把每种药材按结束时间从小到大排序,剩下的模型就是2个背包,且物品只能选一个背包放。

f[i][j]表示一个坩埚到 i 时间,另一个坩埚到 j 时间的最大药效。

如果某药材的结束时间在 i 范围内,则 f[i][j] = max(f[i][j], f[medic[k].st - 1][j] + medic[k].w) ;

若在 j 范围内,则f[i][j] = max(f[i][j], f[i][medic[k].st - 1] + medic[k].w),最后输出f[t][t]

以下是代码:

#include 
using namespace std;
int t, n, f[520][520] = {};
struct pog
{
	int st, en, w;
}medic[120];
bool mycmp(pog x, pog y)
{
	return x.en < y.en;
}
int main()
{
	freopen("medic.in", "r", stdin);
	freopen("medic.out", "w", stdout);
	cin >> t >> n;
	for (int i = 1; i <= n; i++)
    cin >> medic[i].st >> medic[i].en >> medic[i].w;
	sort(medic + 1, medic + 1 + n, mycmp);
	for (int k = 1; k <= n; k++)
	for (int i = t; i >= 0; i--)
	for (int j = t; j >= 0; j--)
	{
		if (medic[k].en <= i) f[i][j] = max(f[i][j], f[medic[k].st - 1][j] + medic[k].w);
		if (medic[k].en <= j) f[i][j] = max(f[i][j], f[i][medic[k].st - 1] + medic[k].w);    
	}
	cout << f[t][t] << endl;
	return 0;
}

(五).区间动规

是阶段以区间长度划分的一类动规问题。最明显的特点是大区间是由小区间推导出来的。

区间动规的基本状态:f[i][j] 表示从第 i 个元素到第 j 个元素这个区间的最优值是多少。

例题 石子合并

题目描述

在操场上沿一直线排列着n堆石子。现要将石子有次序地合并成一堆。

规定每次只能选相邻的两堆石子合并成新的一堆,并将新的一堆石子数计为该次合并的得分。 我们希望这n-1次合并后得到的得分总和最小。

输入格式

第一行有一个正整数n(n<=300),表示石子的堆数; 第二行有n个正整数,表示每一堆石子的石子数,每两个数之间用一个空格隔开。它们都不大于10000。

输出格式

一行,一个整数,表示答案。

样例数据

input

3

1 2 9

output

15

数据规模与约定

时间限制:1s
空间限制:256MB

假设先把1 ~ k合并成一堆,再把k ~ n 合并成一堆,而1 ~ k,k ~ n 的最优解存在重叠的子问题。

w[i,j]表示从i到j的石子数和,f[l,r]表示l~r之间得分总和的最小值。

状态转移方程为 f[l,r] = min{f[l,r], f[l,k] + f[k+1,r]+w[i][j]}

以下为代码:

#include
using namespace std;
int n, a[310], w[310][310], f[310][310];
int main()
{
	freopen("Stone.in","r",stdin);
	freopen("Stone.out","w",stdout);
	memset(f, 10, sizeof(f));
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
		f[i][i] = 0;
	}
	for (int i = 1; i <= n; i++)
	for (int j = i; j <= n; j++)
	w[i][j] = w[i][j - 1] + a[j];
    for (int i = 2; i <= n; i++)
    for (int l = 1; l <= n + 1 - i ;l++)
	{
		int r = l + i - 1;
		for (int k = 1; k < r; k++)
		f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r]);
		f[l][r] += w[l][r];
	}
	cout << f[1][n] << endl;
	return 0;
}

(六)二维动规

例题 马拦过河卒

题目描述

棋盘上A点有一个过河卒,需要走到目标B点。卒行走的规则:可以向下、或者向右。同时在棋盘上C点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为“马拦过河卒”。

棋盘用坐标表示,A点(0, 0)、B点(n, m)(n, m为不超过15的整数),同样马的位置坐标是需要给出的。现在要求你计算出卒从A点能够到达B点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。

输入格式

一行四个数据,分别表示B点坐标和马的坐标。

输出格式

一个数据,表示所有的路径条数。

样例数据

input

6 6 3 3

output

6

数据规模与约定

时间限制:1s
空间限制:256MB

要到达某一个点,只能从左边或上面到,根据加法原理可以得到:

在某个位置上,如果没有马,则f[i][j] = f[i - 1][j] + f[i][j - 1]

以下为代码:

#include 
using namespace std;
const int dx[9] = {0, -1, -2, -2, -1, 1, 2, 1, 2};
const int dy[9] = {0, -2, -1, 1, 2, -2, -1, 2, 1};
int f[30][30] = {}, bx, by, hx, hy;
bool a[30][30] = {};
int main()
{
    freopen("input.in", "r", stdin);
    freopen("output.out", "w", stdout);
    cin >> bx >> by >> hx >> hy;
    for (int i = 0; i <= 8; i++)
    if (hx + dx[i] >= 0 && hy + dy[i] >= 0) a[hx + dx[i]][hy + dy[i]] = true;
    f[0][0] = 1;
    for (int i = 1; i <= max(bx, by); i++)
    {
        if (!a[i][0] && f[i - 1][0]) f[i][0] = 1;
        if (!a[0][i] && f[0][i - 1]) f[0][i] = 1;
    }
    for (int i = 1; i <= bx; i++)
    for (int j = 1; j <= by; j++)
    if (!a[i][j]) f[i][j] = f[i - 1][j] + f[i][j - 1];
    cout << f[bx][by] << endl;
    return 0;
}

二维DP在思维上比线性DP要求更大,所以要多做题。

(七)树形DP

就是在树上进行动规。

一般以节点从浅到深(子树从小到大)的顺序作为DP的阶段,在DP状态的表示中,第一维通常是节点编号(代表以该节点为根的子树)。

我们大多采用递归的方式实现树形DP。对于每个节点x,先递归,在它的每个子节点上DP,在回溯时,从子节点向节点x进行状态转移。

模板题1 树的深度和子树大小

题目描述

给定一棵n个点的无权树,问树中每个节点的深度和每个子树的大小? (以1号点为根节且深度为0)

输入格式

第1行:n。
第2~n行:每行两个数x,y,表示x,y之间有一条边。

输出格式

n行,每行输出格式为:#节点编号 deep:深度 count:子树节点数(详见样例)

样例数据

input

7

1 2

2 3

1 4

3 5

1 6

3 7

output

#1 deep:0 count:7

#2 deep:1 count:4

#3 deep:2 count:3

#4 deep:1 count:1

#5 deep:3 count:1

#6 deep:1 count:1

#7 deep:3 count:1

数据规模与约定

15% n<=10;
40% n<=1000;
100% n<=100000;

时间限制:1s
空间限制:256MB

代码如下:

#include 
using namespace std;
int ans = 0, n, x, y, head[100010] = {};
int co[100010] = {}, deep[100010] = {};
struct zyl
{
    int to, next;
}f[10001000];
void Add(int xx, int yy)
{
    ans++;
    f[ans].to = xx;
    f[ans].next = head[yy];
    head[yy] = ans;
}
void dfs(int x, int fa, int depth)
{
    co[x] = 1;
    deep[x] = depth;
    for (int i = head[x]; i; i = f[i].next)
    {
        int v = f[i].to;
        if (v == fa) continue;
        dfs(v, x, depth + 1);
        co[x] += co[v];
    }
}
int main()
{
    freopen("tree.in", "r", stdin);
    freopen("tree.out", "w", stdout);
    memset(f, 0, sizeof(f));
    scanf("%d", &n);
    for (int i = 1; i < n; i++)
    {
        scanf("%d%d", &x, &y);
        Add(x, y);
        Add(y, x);
    }
    dfs(1, 0, 0);
    for (int i = 1; i <= n; i++)
    {
        printf("#%d deep:%d count:%d\n", i, deep[i], co[i]);
    }
    return 0;
}

模板题2 每个子树点权和最大点权值

题目描述

给定一颗n个点的点权树,问树中每个子树的点权和,点权最大值。n≤105

输入格式

第一行输入一个正整数n(2≤n≤105);

第二行输入n个正整数d(1≤d≤2700000000),表示每点的权值。

第三行到第n+1行输入每条边的信息,每行输入两个整数u(1≤u≤n),v(1≤v≤n)。分别表示边的起点、终点。

数据保证输入的是一棵树。

输出格式

共两行; 第一行输出n个数,表示当前点所包含最大子树的点权和;
第二行输出n个数,表示当前点所包含最大子树的最大点权;
不过滤行末空格

样例数据

input

5

1 3 6 8 7

1 2

1 3

3 4

2 5

output

25 10 14 8 7

8 7 8 8 7

数据规模与约定

数据范围规定 第1、2组数据:n<=10;
第3组数据:100<=n<=50;
第4组数据:500<=n<=1000;
第5组数据:1000<=n<=5000;
第6组数据:n=75000;
第7组数据:10000<=n<=50000;
第8组数据:50000<=n<=100000;
第9、10组数据:n=100000;

时间限制:1s1s
空间限制:256MB

每个子树的点权就是以x为根节点的子树中每个节点的点权和。

以x为根节点的子树中的最大点权便是所有以它儿子为根节点的子树中的最大点权的最大值。

以下为代码:

#include 
using namespace std;
long long maxx = 0, ans = 0, n, x, y, head[100010] = {};
long long d[100010] = {}, s[100010] = {};
long long sum[100010];
struct zyl
{
    long long to, next;
}f[10001000];
void Add(int xx, int yy)
{
    ans++;
    f[ans].to = xx;
    f[ans].next = head[yy];
    head[yy] = ans;
}
void dp(int xx, int fa)
{
    d[xx] = s[xx] = sum[xx];
    for (int i = head[xx]; i; i = f[i].next)
    {
        if (f[i].to == fa) continue;
        dp(f[i].to, xx);
        d[xx] += d[f[i].to];
        s[xx] = max(s[xx], s[f[i].to]);
    }
}
int main()
{
    freopen("t20.in", "r", stdin);
    freopen("t20.out", "w", stdout);
    memset(f, 0, sizeof(f));
    scanf("%lld", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%lld", &sum[i]);
    }
    for (int i = 1; i < n; i++)
    {
        scanf("%lld%lld", &x, &y);
        Add(x, y);
        Add(y, x);
    }
    dp(1, 0);
    for (int i = 1; i < n; i++)
    {
        printf("%lld ", d[i]);
    }
    printf("%lld \n", d[n]);
    for (int i = 1; i < n; i++)
    {
        printf("%lld ", s[i]);
    }
    printf("%lld \n", s[n]);
    return 0;
}

就先写到这里,还有一部分的内容没有写到,等之后有时间再来补充。

DP是一块比较重要的内容,最好的方式就是多练,熟能生巧。

你可能感兴趣的:(DP,专题总结系列)