算法:动态规划(附例题)
动态规划是程序竞赛中出现最多的设计范式之一,要想使用动态规划解决问题,首先要明白解决动态规划问题的类型
重复子问题性质
在被解决的问题中,可以被分为很多的子问题,和分治法不同,动态规划中的有些子问题的计算结果可以用来计算多个问题的解,把这些子问题的计算结果保存起来,重复利用这些计算结果,避免程序的重复调用和计算。
适用于制表的情况
程序函数与数学函数不同,对于数学函数,我们输入一个值,就会有固定的值输出,如果我们设计的程序函数能满足数学函数的情况,每次输入一个值就会输出固定的值,那么我们就可以将这种输入情况的计算结果保存起来,方便下次再重复用到这个结果是直接查表。所谓制表,就是将计算结果保存到表中,方便查询。
解题步骤
1首先分析问题,看问题是否能够分解成多个子问题。
2再分析运行过程中是否会有重复利用的子问题。
3设计穷举法的递归函数,并制表保存重复子问题的结果。
4修改原来的穷举法递归函数,将能够查表的部分,设计为直接查表
例题
例题1跳跃字符
在n*n大小的棋盘中,每个格子都标有1-9当中的一个整数,游戏规则是,从棋盘的最左上角出发,最终到达棋盘的最右下角,此过程中按照棋盘格子中数字,向下或向左移动,但不能走出棋盘,判断是否能够走出棋盘。
输入和输出都在程序里
代码
#include<iostream>
using namespace std;
int cae[7][7];
int board[7][7] =
{
{2,5,1,6,1,4,1},
{6,1,1,2.2,9,3},
{7,2,3,2,1,3,1},
{1,1,3,1,7,1,2},
{4,1,2,3,4,1,2},
{3,3,1,2,3,4,1},
{1,5,2,9,4,7,0},
};
int jump(int x, int y)
{
if (x > 6 || y > 6)
{
return 0;
}
if (board[x][y] == 0)
{
return 1;
}
int& ret = cae[x][y];//制表的过程
if (ret != -1)//查表的过程
{
return ret;
}
int jupszie = board[x][y];
return ret = (jump(x, y + jupszie) || jump(x + jupszie, y));
}
int main()
{
int i = 0;
int j = 0;
for (i = 0; i < 7; i++)
{
for (j = 0; j < 7; j++)
{
cae[i][j] = -1;
}
}
int a=jump(0, 0);
for (i = 0; i < 7; i++)
{
for (j = 0; j < 7; j++)
{
cout << cae[i][j] << " ";
}
cout << endl;
}
cout << a;
}
想了解动态规划算法,我个人觉得最重要的就是能够了解上面代码中数组cae[]中的值是如何变化的,当你能推测出数组中每个值如何变化得时候也就大概了解的动态规划算法。
2通配符
给定带通配符的字符串和多个文件名,按要求进行匹配
要求:
匹配规则中包含通配符?和*,其中?表示匹配任意一个字符,*表示匹配任意多个(>=0)字符。
我在代码中简化了输入,只测试一个输入,因为多个文件名的匹配合单个文件名匹配是一样,只是加一个循环。代码中有输入和输出测试。
分析
制表法本质是存储重复子问题的计算结果,所以很重要的一点就在于
找到重复的子问题,这样我们才能够决定如何存储结果和以什么结构存储结果
本题重复计算在于通配符“”代表的字符的个数,例如范式为**a,和字符串
aaaaaaaaaab进行匹配时,当第一个和a进行匹配,w和s都为0,发现str1【0】为“”,
程序会把w+1,递归调用函数进行下一个match,一直到范式最后一个,再次w+1并递归调用match
后str1[w]“a”,与str2[s]“a”(s此时为0)对比,但是看上面函数可以知道,此时范式到达了最后一个字符,
而字符串刚刚到第一个(s==0),对比失败,然后回溯到第一次调用递归,将skip+1(看代码),
这一次对比过程中会有很多次对比和skip=0时进行的对比过程有很多重复,所以我们将每一次的对比
过程的结果保存起来,这样我们下一次对比的时候就可以直接查表返回结果。
注意:当然有人会想直接简单的递归循环也可以解决问题不保存结果,但是如果输入范式和字符串
都很复杂,就不能很快的解决问题,超出时间限制。
代码
#include<iostream>
using namespace std;
#define M 5
#define N 5
//保存重复计算的子问题的结果
int cae[M][N];
string str1;
string str2;
void chushihua()
{
int i = 0, j = 0;
for (i = 0; i < M; i++)
{
for (j = 0; j < N; j++)
{
cae[i][j] = -1;
}
}
}
int match(int w, int s)
{
int& ret = cae[w][s];
if (ret != -1)
{
return ret;
}
while (w < str1.size() && s < str2.size() && (str1[w] == '?' || str1[w] == str2[s]))
//判断通配符范式和字符串是否相同
{
w++;
s++;
}
//此时会产生两种情况,1范式到最后一个字符,看此时字符串是否到最后一个字符
//2范式到达倒数第二个字符,但是最后一个字符是 “*”,因为“*”可以代表任意个字符,所以也符合条件;
if (w == str1.size()||(w==str1.size()-1&&str1[w]=='*'))
{
return ret=(s == str2.size());
}
//遇到*时循环结束的处理;
int skip = 0;//skip作用很大,利用skip找出*代表了多少个字符
if (str1[w] == '*')
{
//在下面的循环中产生重复。
for (skip = 0; skip + s < str2.size(); skip++)
{
if (match(w + 1, s + skip))
{
return ret=1;
}
}
}
return ret=0;
}
int main()
{
cout << "输入字符串" << endl;
//cin >> str2;
str2 = "helio";//要匹配的字符串
str1 = "*p*";//通配符范式
cout << "输入范式" << endl;
//cin >> str1;
chushihua();
int flag = match(0, 0);
cout << flag << endl;
int i = 0, j = 0;
for (i = 0; i < M; i++)
{
for (j = 0; j < N; j++)
{
cout << cae[i][j];
}
cout << endl;
}
}
/*
小结:
制表法本质是存储重复子问题的计算结果,所以很重要的一点就在于
找到重复的子问题,这样我们才能够决定如何存储结果和以什么结构存储结果
本题重复计算在于通配符“*”代表的字符的个数,例如范式为******a,和字符串
aaaaaaaaaab进行匹配时,当第一个*和a进行匹配,w和s都为0,发现str1【0】为“*”,
程序会把w+1,递归调用函数进行下一个match,一直到范式最后一个*,再次w+1并递归调用match
后str1[w]==“a”,与str2[s]==“a”(s此时为0)对比,但是看上面函数可以知道,此时范式到达了最后一个字符,
而字符串刚刚到第一个(s==0),对比失败,然后回溯到第一次调用递归,将skip+1(看代码),
这一次对比过程中会有很多次对比和skip=0时进行的对比过程有很多重复,所以我们将每一次的对比
过程的结果保存起来,这样我们下一次对比的时候就可以直接查表返回结果。
注意:当然有人会想直接简单的递归循环也可以解决问题不保存结果,但是如果输入范式和字符串
都很复杂,就不能很快的解决问题,超出时间限制。
*/
/*
想要看懂本题就要看懂cae中产生的结果,很重要
*/
最优子结构类问题
x下面的3 4题都是具有最优子结构类的问题,这类问题可以在原来的直接制表的
方法上进行程序的优化,能够优化的原因是,这类问题,无论上一步进行的如何不会影响到下一步的计算结果。下一步问题的解和上一步问题的解无关。
例题3最长递增子序列
问题描述很简单,可自行百度
代码
#include<iostream>
#include<algorithm>
using namespace std;
int cae[10];
int s[10];
int len;
int lis(int start)
{
int& ret = cae[start];//制表,保存计算完的结果,下次计算时直接查表
if (ret != 0)
{
return ret;
}
ret = 1;//当循环结束时,如果ret的值没有改变,
//说明此时是最后一个字符,所以长度为1,注意不能在最后将ret的值赋值为1,会有逻辑错误。
int i = start + 1;
for (; i < len; i++)
{
if (s[i] > s[start])
{
ret = max(ret, lis(i) + 1);
}
}
return ret ;//循环结束表示当前只有一个数满足条件,也是整个递归的出口
}
int main()
{
int i = 0;
int a = 0;
cout << "输入数组" << endl;
cin >> a;
while (a!= 0)
{
s[i] = a;
cin >>a;
i++;
}
len = i;
int res = 0;
res = lis(0);
cout << "最长子序列为:" << res<<endl;
}
重要的东西都在代码的注释里
4三角形路径最大值
问题自行百度,很简单。输入和输出在注释中,重要的东西也在注释中
代码
#include<iostream>
#include <algorithm>
using namespace std;
#define N 5
#define M 5
/*
测试输入:
6 -1
1 2 -1
3 7 4 -1
9 4 1 7 -1
2 7 5 9 4 -1
0
(输入-1结束本行,输入0结束全部输入)
测试输出:
28
*/
int cae[10][10];
int tange[10][10];
void chushihua()
{
int i = 0;
int j = 0;
for (i = 0; i <N; i++)
{
for (j = 0; j < N; j++)
{
tange[i][j] = -1;
cae[i][j] = -1;
}
}
}
int path(int x, int y)
{
int& ret = cae[x][y];//制表
if (ret != -1)
{
return ret;
}
if (x == N - 1)
{
return ret = tange[x][y];
}
return ret = tange[x][y] + max (path(x + 1, y), path(x + 1, y + 1));
}
int main()
{
int i = 0, j = 0;
int a = 0;
chushihua();
cout << "输入三角矩阵,输入-1结束本行输入,输入0结束全部输入" << endl;
cin >> a;
while (a != -1&&a!=0)
{
while (a!=- 1)
{
tange[i][j] = a;
j++;
cin >> a;
}
j = 0;
cin >> a;
i++;
}
int res = 0;
res=path(0, 0);
cout << res << endl;
//最后输出一下保存结果的数表
for (i = 0; i < N; i++)
{
for (j = 0; j < M; j++)
{
if (cae[i][j] != -1)
{
cout << cae[i][j] << " ";
}
}
cout << endl;
}
}
例题5背诵圆周率
背诵圆周率时可以将圆周率数字划分不同的组降低难度,下面有4个分类情况,每个分类有不同的难度,将圆周率划分完后将难度加在一起就是此时的难度,要求给定长度为n的数字后,每次划分3个到5个数字,求划分后的最小难度
- 所有数字相同 333 555 难度 1
- 数字逐个递减 23456 难度 2
- 两个数字交替出现 323 4545 难度 4
- 等差数列 147 难度 5
- 其他 情况 17912 331 难度 10
测试
输入 1 2 3 4 1 2 3 4
输出 2
代码
#include<iostream>
#include<vector>
#include<algorithm>
#define N 100
using namespace std;
int cae[N];
vector<int> pi;
int classfiy(int start,int l)
{
if (l == 3)
{
if (pi[start] == pi[start + 1] && pi[start] == pi[start + 2])
{
return 1;
}
if (pi[start + 1] == pi[start] + 1 && pi[start + 2] == pi[start + 1] + 1)
{
return 2;
}
if (pi[start] == pi[start + 2])
{
return 4;
}
if (pi[start + 1] - pi[start] == pi[start + 2] - pi[start + 1])
{
return 5;
}
return 10;
}
if (l == 4)
{
if (pi[start] == pi[start + 1] && pi[start] == pi[start + 2]&&pi[start]==pi[start+3])
{
return 1;
}
if (pi[start + 1] == pi[start] + 1 && pi[start + 2] == pi[start + 1] + 1&&pi[start+3]==pi[start+2]+1)
{
return 2;
}
if (pi[start] == pi[start + 2]&&pi[start+1]==pi[start+3])
{
return 4;
}
//int sum = 0;
//int res = 0;
//res = (l * (pi[start] + pi[start + l - 1]) )/ 2;
//for (int i = 0; i < l; i++)
//{
// sum = sum + pi[start + i];
//}
int res = 0;
res = pi[start + 1] - pi[start];
if (res == pi[start + 2] - pi[start + 1] && pi[start + 3] - pi[start + 2] == res)
{
return 5;
}
return 10;
}
if (l == 5)
{
if (pi[start] == pi[start + 1] && pi[start] == pi[start + 2] && pi[start] == pi[start + 3]&&pi[start]==pi[start+4])
{
return 1;
}
if (pi[start + 1] == pi[start] + 1 && pi[start + 2] == pi[start + 1] + 1 && pi[start + 3] == pi[start + 2] + 1&&pi[start+4]==pi[start+3]+1)
{
return 2;
}
if (pi[start] == pi[start + 2] && pi[start + 1] == pi[start + 3]&&pi[start]==pi[start+4])
{
return 4;
}
int res = 0;
res = pi[start + 1] - pi[start];
if (res == pi[start + 2] - pi[start + 1]&&pi[start+3]-pi[start+2]==res&&pi[start+4]-pi[start+3]==res)
{
return 5;
}
return 10;
}
}
int minhard(int start)
{
int& ret = cae[start];
if (ret != 100)
{
return ret;
}
int l = 3;
for (; l < 6; l++)
{
if (start + l < pi.size()+1)
{
ret = min(ret, classfiy(start, l) + minhard(start + l));
}
if(start==pi.size())
{
return 0;//当最后不能够继续划分时直接返回难度10,作为初始部分,就不在上面的函数中处理。
}
else if(start+l>pi.size())
{
return ret=min(ret, 10);
}
}
return ret;
}
int main()
{
int i = 0;
int a = 0;
cout << "输入数字输入-1结束以空格为间隔:" << endl;
cin >> a;
while (a!=-1)
{
pi.push_back(a);
cin >> a;
}
for (i = 0; i < 10; i++)
{
cae[i] = 100;
}
int res = 0;
res = minhard(0);
cout << endl << "最小难度为:" << res << endl;
//while (i < pi.size())
//{
// cout << pi[i];
// i++;
//}
}
例题6盖板方法个数
用21大小的瓷砖铺盖2n大小的长方形,共有多少种方法。
分析
这道题关键在于问题的转化,2width的白板,每次盖
21的板,每次盖上一块板时,无论是横盖还是竖盖,都会使
整个板的长width,减少一或或2,并且,无论你前一次如何盖板,
不会影响到后一次的盖板的方法数,所以问题可以转化为:
til(n)=当长度为n时能盖板的方法
til(n)=til(n-1)+til(n-2);
处理好初始部分和递归关系就OK。
代码
#include<iostream>
using namespace std;
#define M 10000000
int n;
int cae[10];//用二维数组表示要盖的板,盖住为1,没盖住为0
int til(int width)
{
int& ret = cae[width];
if (ret != 0)
{
return ret;
}
if (width < 2)
{
return 1;
}
ret = til(width - 1) + til(width - 2);
return ret;
}
int main()
{
cout << "请输入板的长度" << endl;
cin >> n;
int res = 0;
res = til(n);
cout << "方法有:" <<res <<endl;
}
/*
这道题关键在于问题的转化,2*width的白板,每次盖
2*1的板,每次盖上一块板时,无论是横盖还是竖盖,都会使
整个板的长width,减少一或或2,并且,无论你前一次如何盖板,
不会影响到后一次的盖板的方法数,所以问题可以转化为:
til(n)=当长度为n时能盖板的方法
til(n)=til(n-1)+til(n-2);
处理好初始部分和递归关系就OK。
*/
例题7爬出水井的蜗牛
一只蜗牛在深度为n米的井里,蜗牛想爬出去,由于天气原因每次只能爬1米或者2米,每天是阴天和晴天的概率各为50%,问蜗牛能否在m天内爬出。
分析
定义climd(day,climbed)=蜗牛在day天爬行了climbed米时能够在mt天内爬行n米的个数,所以
climb(day,climbed)=climb(day+1,climbed+1)+climb(day+1,climbed+2);
测试
输入 n 10
m 7
输出 99
代码
#include<iostream>
using namespace std;
int cae[100][100];
int m;
int n;
int climb(int day, int climbed)
{
if (day == m)
{
if (climbed > n-1)
{
return 1;
}
return 0;
}
int& ret = cae[day][climbed];
if (ret != 0)
{
return ret;
}
return ret = climb(day + 1, climbed + 1) + climb(day + 1, climbed + 2);
}
int main()
{
cout << "请输入水井高度" << endl;
cin >> n;
cout << "请输入天数" << endl;
cin >> m;
int res = climb(0, 0);
cout << res << endl;
}