动态规划
算法思想
动态规划是一种解决最优化问题的算法,较为官方的讲,动态规划是一种能够将一个复杂的问题分解为若干个子问题,最后综合子问题的最优解来得到原问题最优解的过程。
即这类问题所要考虑的重点便为能否划分为子问题解决以及如何划分这两大问题。
能否划分为子问题解决
首先考虑能否划分,即动态规划算法所具有的共性,重复子问题以及最优子结构。
重复子问题: 即我们所要求解的问题中对于这个问题的过程值我们需要反复进行计算获取来使用,这种情况我们往往通过记录下子问题值的方式来进行算法时间上的优化,典型例子:斐波那契数列。
**最优子结构:**所求问题的最优解包含子问题的最优解。
如何划分
找准状态的定义以及状态转移方程的建立,在此基础上明确边界以及赋值顺序等细节问题。
应用实例
一、矩阵链相乘问题 (输出最终结合方式)
实现思路
状态的定义:dp[i][j] : 表示为从第i个矩阵乘到第j个矩阵所需要的最少的乘法次数。
此题的最优化点为矩阵相乘所需要的最少的乘法次数,将问题更加具体来讲便是求得从第0个矩阵到最后一个矩阵所需的最少乘法次数,因此,我们不难想到,它的子问题既是小规模情况下所需要的最少的乘法次数,思考到这里以后,接下来便是思考有了小规模的最优解能否求得大规模的最优解,以及小规模的最优解会不会受到矩阵增加的影响。
小规模的最优解显然不会受到矩阵增加的影响,因为无论矩阵增加多少个,这个小规模是一定的,定义好了的,它的状态不会发生改变。接下来,便是思考递推公式即状态转移方程的确定。
状态转移方程:dp[i][j] = min( i <= k < j)(dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])
有了状态转移方程后,整体思路已经差不多成型了,还有一点细节但也非常重要的便是边界的确定,即初始化以及求值的顺序问题。
由于该问题我们最终所要求解的问题是第0个矩阵到最后一个矩阵的最优化问题,即子问题是因此在赋值顺序的过程中,我们需要拥有,中间段的子问题的值,正三角显然行不通,因此采取倒三角的赋值方式。
代码实现
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// U1U2U3U4U5U6;2,3,7,9,5,2,4
#define MAXNUM 1e6
int N;
vector< vector<int> > dp;
vector< vector<int> > show;
struct node {
int a;
int b;
};
struct node num[1000];
void add(int i, int j, int k)
{
if (j - i <= 0)
return;
//在知道了,i,j,k;判断他们三者的距离来决定要不要加括号
if (k - i > 0)
{
num[i].a++;
num[k].b++;
}
if (j - k > 1)
{
num[k + 1].a++;
num[j].b++;
}
add(i, k, show[i][k]);
add(k + 1, j, show[k + 1][j]);
}
void matrix_multi(string input) {
bool is_num = false;
vector<int> matrix_dimension;
for(int i = 0;i < input.size(); ++i) {
if(input[i] == ';') {
is_num = true;
continue;
}
if(!is_num) {
continue;
}
int tmp_value = 0;
while(input[i] != ',') {
tmp_value = tmp_value*10 + (input[i] - '0');
++i;
if (i >= input.size()) {
break;
}
}
matrix_dimension.push_back(tmp_value);
}
N = matrix_dimension.size() - 1;
dp.resize(N+1,vector<int>(N+1, MAXNUM));
show.resize(N+1, vector<int>(N+1, 0));
for (int i = 1; i <= N; ++i) {
dp[i][i] = 0;
}
for (int i = N; i >= 1; --i) {
for (int j = i + 1; j <= N; ++j) {
dp[i][j] = dp[i][i] + dp[i + 1][j] + matrix_dimension[i - 1]*matrix_dimension[i]*matrix_dimension[j];
show[i][j] = i;
for (int k = i + 1; k < j; ++k) {
int tmp_val = dp[i][k] + dp[k + 1][j] + matrix_dimension[i - 1]*matrix_dimension[k]*matrix_dimension[j];
if (tmp_val < dp[i][j]) {
dp[i][j] = tmp_val;
show[i][j] = k;
}
}
}
}
add(1, N, show[1][N]);
printf("(");
for (int i = 1; i <= N; i++)
{
while (num[i].a != 0)
{
num[i].a--;
printf("(");
}
printf("U%d", i);
while (num[i].b != 0)
{
num[i].b--;
printf(")");
}
}
printf(")");
//cout << dp[1][N] << endl;
}
int main() {
string input;
cin >> input;
matrix_multi(input);
return 0;
}
输入以及输出结果:
输入:U1U2U3U4U5U6;2,3,7,9,5,2,4
输出:
二、钢条切割问题
实现思路
状态的定义:dp[i]:钢条长度为i时切割所能获得的最大收益。
状态转移方程:dp[i] = max(0 <= j < i)(dp[i-j] + dp[j])
赋值顺序:正三角
代码实现:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// 10;1,1;2,5;3,8;4,9;5,10;6,17;7,17;8,20;9,24;10,24;
int N = -1;
vector<int> value;
vector<int> dp;
void print() {
for(int i = 0;i < value.size(); ++i) {
cout << value[i] << " ";
}
cout << endl;
}
void stealbar_splite(string input) {
for (int i = 0; i < input.size(); ++i) {
if(N == -1) {
int tmp_val = 0;
while (input[i] != ';') {
tmp_val = tmp_val*10 + (input[i] - '0');
++i;
}
N = tmp_val;
continue;
}
bool is_num = false;
while(input[i] != ';') {
if (input[i] == ',') {
is_num = true;
++i;
continue;
}
if(!is_num) {
++i;
continue;
}
int tmp_val = 0;
while(input[i] != ';') {
tmp_val = tmp_val*10 + (input[i] - '0');
++i;
}
value.push_back(tmp_val);
}
}
dp.resize(N + 1, 0);
dp[1] = value[0];
// 算法核心
for (int i = 2;i <= N; ++i) {
dp[i] = value[i - 1];
for (int j = 1; j < i; ++j) {
if (dp[i] < dp[j] + dp[i - j]) {
dp[i] = dp[j] + dp[i - j];
}
}
}
cout << dp[N] << endl;
}
int main() {
string input;
cin >> input;
stealbar_splite(input);
return 0;
}
输入以及输出结果
输入:10;1,1;2,5;3,8;4,9;5,10;6,17;7,17;8,20;9,24;10,24;
输出:27
三、最长公共子串(输出公共子串)
实现思路
状态的定义:dp[i][j]:第一个字符串前i个字符与第二个字符串前j个字符的最长子串数目。
状态转移方程:dp[i][j] = (v[i] == v[j]) ? (dp[i-1][j-1] + 1) : 0
赋值顺序:正矩形
代码实现:
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// A,B,E,A,D,F,C;A,C,E,A,D,B
vector<char> str1;
vector<char> str2;
vector< vector<int> > dp;
void common_substring_longest(string input) {
bool is_secondstr = false;
for (int i = 0; i < input.size(); ++i) {
if (input[i] == ';') {
is_secondstr = true;
continue;
}
if (!is_secondstr) {
if (input[i] != ',') {
str1.push_back(input[i]);
}
continue;
}
if (input[i] != ',') {
str2.push_back(input[i]);
}
}
int n1 = str1.size();
int n2 = str2.size();
dp.resize(n1 + 1, vector<int>(n2 + 1, 0));
int max_v = -1;
int l_index = 0;
int r_index = 0;
// 算法核心
for (int i = 1; i <= n1; ++i) {
for (int j = 1; j <= n2; ++j) {
if (str1[i - 1] == str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else {
dp[i][j] = 0;
}
if (max_v < dp[i][j]) {
max_v = dp[i][j];
l_index = i;
r_index = j;
}
}
}
int l_begin = l_index - max_v;
for(int i = l_begin; i < l_index; ++i) {
cout << str1[i];
if (i != l_index - 1) {
cout << ",";
}
}
}
int main() {
string input;
cin >> input;
common_substring_longest(input);
return 0;
}
输入以及输出
输入:A,B,E,A,D,F,C;A,C,E,A,D,B
输出:E、A、D
四、最长公共子序列(输出最长子序列)
实现思路
状态的定义:dp[i][j]:第一个字符串前i个字符与第二个字符串前j个字符的最长公共子序列数目。
状态转移方程:dp[i][j] = (v[i] == v[j]) ? (dp[i-1][j-1] + 1) : max(dp[i-1][j], dp[i][j-1])
赋值顺序:正矩形
代码实现:
#include<iostream>
#include<vector>
#include<algorithm>
#include<queue>
using namespace std;
// A,B,C,B,D,A,B;B,D,F,A,B,A
vector<char> str1;
vector<char> str2;
vector< vector<int> > dp;
vector< vector<int> > output;
queue<char> ans;
void print(int x, int y) {
if (x >= 0 && y >= 0) {
if(output[x][y]==1)
{
print(x-1,y-1);
ans.push(str1[x]);
}
if(output[x][y]==2)
print(x - 1, y);
if(output[x][y]==3)
print(x, y - 1);
}
}
void common_subsequence_longest(string input) {
bool is_secondstr = false;
for (int i = 0; i < input.size(); ++i) {
if (input[i] == ';') {
is_secondstr = true;
continue;
}
if (!is_secondstr) {
if (input[i] != ',') {
str1.push_back(input[i]);
}
continue;
}
if (input[i] != ',') {
str2.push_back(input[i]);
}
}
int n1 = str1.size();
int n2 = str2.size();
dp.resize(n1 , vector<int>(n2, 0));
output.resize(n1 , vector<int>(n2, 0));
// 算法核心
for (int i = 0;i < n1; ++i) {
for (int j = 0; j < n2; ++j) {
if (str1[i] == str2[j]) {
if (i == 0 || j == 0) {
dp[i][j] = 1;
}
else {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
output[i][j] = 1;
}
else {
if (i == 0 && j == 0) {
dp[i][j] = 0;
}
else if (i == 0 && j != 0) {
dp[i][j] = dp[i][j-1];
output[i][j] = 3;
}
else if (i != 0 && j == 0) {
dp[i][j] = dp[i - 1][j];
output[i][j] = 2;
}
else {
if (dp[i - 1][j] > dp[i][j - 1]) {
output[i][j] = 2;
dp[i][j] = dp[i - 1][j];
}
else {
output[i][j] = 3;
dp[i][j] = dp[i][j - 1];
}
}
}
}
}
print(n1 - 1, n2 - 1);
while(!ans.empty()) {
cout << ans.front();
ans.pop();
if (! ans.empty()) {
cout << ",";
}
}
// cout << dp[n2][n1] << endl;
}
int main() {
string input;
cin >> input;
common_subsequence_longest(input);
return 0;
}
输入以及输出
输入:A,B,C,B,D,A,B;B,D,F,A,B,A
输出:B,D,A,B
五、背包问题
实现思路
状态的定义:dp[i][j]:将前i种商品装入大小为j的背包所能获得的最大价值。
状态转移方程:dp[i][j] = (v[i] > j) ? (dp[i-1][j]) : max(dp[i-1][j], (dp[i - 1][j - v[i] + p[i]))
赋值顺序:正矩形
代码实现:
#include<iostream>
#include<stdlib.h>
#include<string>
#include<vector>
#include<stack>
#include<cmath>
#include<algorithm>
using namespace std;
int C;
vector<int> vi,pi;
int power(int a,int b)
{
int sum = 1;
for(int i = 1; i <= b; i++)
{
sum = sum*a;
}
return sum;
}
void GetArr(string s)
{
int n = s.length();
// 获取C的值
int i = 0;
while(s[i]!=';')
{
C = C*10 + (s[i]-'0');
i++;
}
i++;
// 获取vi,pi的值
int v,p;
while(i<n)
{
v = 0;
p = 0;
while(i<n && s[i]!=',')
{
v = v*10 + (s[i]-'0');
i++;
}
vi.push_back(v);
i++;
while(i<n && s[i]!=';')
{
p = p*10 + (s[i]-'0');
i++;
}
pi.push_back(p);
i++;
}
}
int Max(int a, int b)
{
if(a>b)
{return a;}
else{return b;}
}
int KNAPSACK(int row,int col)
{
vector< vector<int> > dp;
dp.resize(row, vector<int>(col, 0));
for(int i = 1; i < row;i++)
{
for(int j = 1; j < col; j++)
{
dp[i][j]=dp[i-1][j];
if(vi[i]<=j)
{
dp[i][j]= Max(dp[i][j],dp[i-1][j-vi[i]]+pi[i]);
}
}
}
return dp[row-1][col-1];
}
int main()
{
string input;
cin>>input;
//输入格式化
GetArr(input);
// 背包问题求解
int value = KNAPSACK(vi.size()+1,C+1);
cout<<value;
return 0;
}
输入以及输出
输入:13;10,24;3,2;4,9;5,10;4,9;
输出:28
六、最大子数组
实现思路
状态的定义:dp[i]:以第i个数字为结尾的数组的最大子数组和为dp[i]。该题扩展为以最后一个数字结尾的最大子数组的和。
状态转移方程:dp[i] = (dp[i-1] < 0) ? (v[i] : dp[i-1] + v[i])
赋值顺序:顺序遍历
代码实现:
#include<iostream>
#include<stdlib.h>
#include<string>
#include<vector>
#include<stack>
#include<cmath>
#include<algorithm>
using namespace std;
vector<int> arr;
int power(int a,int b)
{
int sum = 1;
for(int i = 1; i <= b; i++)
{
sum = sum*a;
}
return sum;
}
void GetArr(string s)
{
stack<int> store;
int n = s.length();
int i = n-1;
while (i>=0)
{
int count=0;
int sum = 0;
while(i>=0 && s[i]!=',')
{
if(s[i]=='-')
{ sum = -1*sum;
i--;
}
else{
sum += (s[i]-'0')*power(10,count);
count+=1;
i--;
}
}
store.push(sum);
i--;
}
n = store.size();
for(int j = 0; j < n; j++)
{
arr.push_back(store.top());
store.pop();
}
}
int main() {
string input;
cin >> input;
// 输入格式化
GetArr(input);
// 动态规划数组
vector<int> sum;
for(int i = 0; i < arr.size(); i++)
{
sum.push_back(0);
}
sum[0]=arr[0];
for(int i = 1; i < arr.size(); i++)
{
if(sum[i-1]<0)
{
sum[i] = arr[i];
}else{sum[i]=sum[i-1]+arr[i];}
}
// 找最大子数组和
int max_sum=sum[0];
int max_index = 0;
for(int i = 1; i < sum.size();i++)
{
if(sum[i]>max_sum)
{ max_sum = sum[i];
max_index = i;}
}
// 寻找数组坐标范围
int min_index = max_index;
int min_sum = max_sum;
while(min_sum!=0)
{
min_sum = min_sum-arr[min_index];
min_index-=1;
}
cout<<"X["<<min_index+1<<","<<max_index<<"]="<<max_sum<<endl;
return 0;
}
输入以及输出
输入:-1,-3,3,5,-4,3,2,-2,3,6
输出:X[2,9]=16