动态规划的引入
动态规划的引入
引言
可以将动态规划的核心理解为 " 记录 " 和 " 自底向上 "
对于一个问题,如果这个问题能够拆解为同类型的有限个子问题,并且最终的结果能够由这些子问题得到,则可以使用动态规划。
动态规划的实现思想为 " 自底向上 " ,即先求出最小子问题的结果,然后通过这个结果又得到父问题的结果,以此类推,直到得到最终结果。
当然动态规划也可以是 " 记录 " ,即把需要重复计算的值存储下来,等到需要使用的时候就直接用
这样可以减少时间【尤其是在DFS有关的题目中会大大减少耗时】
路径问题
经典路径问题
经典的路径问题给人的感觉更偏重于 " 记录 "
主要是记录每个节点的最大结果,然后用这个结果去求下一个节点的最大结果
洛谷 - P1216 数字三角形 Number Triangles
题目描述
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。
每一步可以走到左下方的点也可以到达右下方的点。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一个行一个正整数 r ,表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
输出格式
单独的一行,包含那个可能得到的最大的和。
样例输入 #1
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
样例输出 #1
30
AC CODE
int main()
{
int n, amap[100][100], flag[100][100];
cin>>n;
for(int h=1; h<=n; h++)
for(int i=1; i<=h; i++)
cin>>amap[h][i];
flag[1][1] = amap[1][1];
for(int h=2; h<=n; h++)
{
for(int i=1; i<=h; i++)
{
if(i == 1)
flag[h][i] = flag[h-1][1] + amap[h][i];
else if(i == h)
flag[h][i] = flag[h-1][i-1] + amap[h][i];
else
flag[h][i] = amap[h][i] + max(flag[h-1][i], flag[h-1][i-1]);
}
}
int anser = 0;
for(int h=1; h<=n; h++)
if(anser < flag[n][h]) anser = flag[n][h];
cout<<anser<<endl;
return 0;
}
需要记录具体方案的问题
记录具体方案的情况需要明白一点:并不需要将方案全部记录下来
即,对于某个节点的最佳方案,并不需要像数组一样记录它的值,是可以记录 它是怎么获得的 或者是 它下一步是去哪里 ,最后再输出方案的时候进行条件判断即可【可以使用递归输出】
洛谷 - P2196 挖地雷
题目描述
在一个地图上有N个地窖,每个地窖中埋有一定数量的地雷。
同时,给出地窖之间的连接路径。
当地窖及其连接的数据给出之后,某人可以从任一处开始挖地雷,
然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。
设计一个挖地雷的方案,使某人能挖到最多的地雷。
样例输入 #1
5
10 8 4 7 6
1 1 1 0
0 0 0
1 1
1
样例输出 #1
1 3 4 5
27
AC CODE
int n; // 地窖的数量
int boom_num[Maxsize]; // 每个地窖中地雷的个数
int amap[Maxsize][Maxsize]; // 地窖连接情况
int record[Maxsize]; // 到第 i 个地窖时能获得的最大地雷数
int read_[Maxsize]; // 第 i 个地窖时上一步的地窖
void FindOrig(int h)
{
if(read_[h] == -1)
{
cout<<h<<" ";
return ;
}
FindOrig(read_[h]);
cout<<h<<" ";
}
int main()
{
cin>>n;
for(int h=1; h<=n; h++) cin>>boom_num[h];
for(int h=1; h<=n; h++)
for(int i=h+1; i<=n; i++)
cin>>amap[h][i];
record[0] = 0;
record[1] = boom_num[1];
for(int h=1; h<=n; h++) read_[h] = -1; // 没有地窖能够通往第 h 个地窖
for(int h=2; h<=n; h++)
{
int tempmax = 0, pos = -1;
for(int i=1; i<h; i++)
{
if(amap[i][h])
{ // 第 i 个地窖能够达到第 h 个地窖
if(record[i] > tempmax)
{
tempmax = record[i];
pos = i;
}
}
}
record[h] = tempmax + boom_num[h];
read_[h] = pos;
}
int anspos = 0, temp = 0;
for(int h=1; h<=n; h++)
{
if(record[h] > temp)
{
temp = record[h];
anspos = h;
}
}
FindOrig(anspos);
cout<<endl<<record[anspos];
return 0;
}
与DFS有关的问题
与DFS相关的动态规划可以完全理解为DFS,只是在求每一个节点值的时候将它记录下来
洛谷 - P1434 滑雪
题目描述
Michael 喜欢滑雪。这并不奇怪,因为滑雪的确很刺激。
Michael 想知道在一个区域中最长的滑坡。
区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子:
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度会减小。
在上面的例子中,一条可行的滑坡为 24-17-16-1(从 24 开始,在 1 结束)。
输入格式
输入的第一行为表示区域的二维数组的行数 R 和列数 C。
下面是 R 行,每行有 C 个数,代表高度(两个数字之间用 1 个空格间隔)。
输出格式
输出区域中最长滑坡的长度。
样例输入 #1
5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
样例输出 #1
25
AC CODE
int amap[107][107]; // 输入数据
int record[107][107]; // h i 点开始,能滑的最大长度
int m, n, anser = 0;
int DFS(int x, int y)
{
if(x <= 0 || y <= 0 || x > m || y > n) // 超出边界
return 0;
if(!record[x][y])
{
int max_around = 0;
if(amap[x][y] < amap[x-1][y])
max_around = max(max_around, DFS(x-1, y));
if(amap[x][y] < amap[x+1][y])
max_around = max(max_around, DFS(x+1, y));
if(amap[x][y] < amap[x][y-1])
max_around = max(max_around, DFS(x, y-1));
if(amap[x][y] < amap[x][y+1])
max_around = max(max_around, DFS(x, y+1));
record[x][y] = 1 + max_around;
}
return record[x][y];
}
int main()
{
cin>>m>>n;
for(int h=1; h<=m; h++)
{
for(int i=1; i<=n; i++)
{
cin>>amap[h][i];
record[h][i] = 0;
}
}
for(int h=1; h<=m; h++)
{
for(int i=1; i<=n; i++)
{
record[h][i] = DFS(h, i);
if(anser < record[h][i])
anser = record[h][i];
}
}
cout<<anser<<endl;
return 0;
}
0-1背包
0-1背包的核心:a[h][i] = max(a[h][i-1], a[h-item.volume][i-1] + item.value);
即,先记录使用 h-item. volume空间,拿前 i-1 种物品的最大价值结果
则,在使用 h 空间拿前 i 种物品时,要么放入第 i 个物品,要么不放
那么,如果要放入这个物品,则得到的价值为a[h-item.volume][i-1] + item.value
如果不放,则其价值为a[h][i-1]
经典0-1背包
洛谷 - P1048 采药
题目描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。
为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。
医师把他带到一个到处都是草药的山洞里对他说:
“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。
我会给你一段时间,在这段时间里,你可以采到一些草药。
如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
样例输入 #1
70 3
71 100
69 1
1 2
样例输出 #1
3
AC CODE
struct item
{
int gettime;
int val;
};
int max(int a, int b)
{
if(a > b) return a;
return b;
}
int main()
{
int time, num;
int record[1007][107]; // 记录最大价值
item itemlist[107];
cin>>time>>num;
for(int h=1; h<=num; h++)
cin>>itemlist[h].gettime>>itemlist[h].val;
for(int h=0; h<=num; h++) record[0][h] = 0;
for(int h=0; h<=time; h++ ) record[h][0] = 0;
for(int h=1; h<=time; h++)
{
for(int i=1; i<=num; i++)
{
if(h >= itemlist[i].gettime)
{
int temp = record[h-itemlist[i].gettime][i-1] + itemlist[i].val;
record[h][i] = max(record[h][i-1], temp);
}
else
{
record[h][i] = record[h][i-1];
}
}
}
cout<<record[time][num]<<endl;
return 0;
}
最大子段和类型
最大子段和
洛谷 - P1115 最大子段和
题目描述
给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。
输入格式
第一行是一个整数,表示序列的长度 n。
第二行有 n 个整数,第 i 个整数表示序列的第 i 个数字。
输出格式
输出一行一个整数表示答案。
样例输入 #1
7
2 -4 3 -1 2 -4 3
样例输出 #1
4
AC CODE
int main()
{
// record 记录前 i 个数据的最大和
int n, array[MaxSize], record[MaxSize], anser = -99999999;
cin>>n;
for(int h=0; h<n; h++) cin>>array[h];
record[0] = array[0];
for(int h=1; h<n; h++)
{
if(record[h-1] + array[h] > array[h])
record[h] = record[h-1] + array[h];
else
record[h] = array[h];
if(anser < record[h]) anser = record[h];
}
if(anser < record[0]) anser = record[0];
cout<<anser<<endl;
return 0;
}
划分数组类型
洛谷 - 1025 数的划分
题目描述
将整数n分成k份,且每份不能为空,任意两个方案不相同(不考虑顺序)。
例如:n=7,k=3,下面三种分法被认为是相同的。
1,1,5;
1,5,1;
5,1,1;
问有多少种不同的分法。
输入格式
n,k (6< n < 200,2 < k < 6)
输出格式
1 个整数,即不同的分法。
样例输入 #1
7 3
样例输出 #1
4
AC CODE
import java.util.Scanner;
class dpMap {
public int num;
public int piece;
public int[][] piece_map;
public dpMap(int n, int p) {
this.num = n;
this.piece = p;
this.piece_map = new int[n + 1][p + 1];
for (int h = 0; h <= n; h++) {
this.piece_map[h][1] = 1; // 把h个1分成1份的方案数为1
}
for (int h = 0; h <= p; h++) {
this.piece_map[h][h] = 1; // 把h个1分成h份的方案数为1
}
for (int h = 0; h <= n; h++) {
this.piece_map[h][0] = 0;
}
for (int h = 0; h <= p; h++) {
this.piece_map[0][h] = 0;
}
}
public void dpPieceMap() {
for (int h = 2; h <= this.num; h++) {
for (int i = 2; i <= this.piece; i++) {
if (h == i) {
this.piece_map[h][i] = 1;
} else if (h < i) {
this.piece_map[h][i] = 0;
} else {
for (int j = 1; j <= i; j++) {
this.piece_map[h][i] += this.piece_map[h - i][j];
}
}
}
}
}
}
public class LuoGu_P1025 {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
int num = scanner.nextInt(), piece = scanner.nextInt();
dpMap dpmap = new dpMap(num, piece);
dpmap.dpPieceMap();
for (int h = 0; h <= num; h++) {
for (int i = 0; i <= piece; i++) {
System.out.print(dpmap.piece_map[h][i] + " ");
}
System.out.println();
}
System.out.println(dpmap.piece_map[num][piece]);
}
}
}
动态策略:
当数多于盘子时:piece_map[h][i] = piece_map_cannull[h-i][i];
piece_map_cannull[h-i][i] = Σpiece_map[h][j];(j∈[1,i])