Dynamic Programming
DP定义:
动态规划是分治思想的延伸,通俗一点来说就是大事化小、小事化无的艺术。
将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并为后面处理更大规模的问题的时候,直接使用这些结果
动态规划具备三个特点:
1、将原来的问题分解为几个相似的子问题
2、所有的子问题只需要解决一次
3、存储子问题的解
动态规划的本质,是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)
动态规划问题一般从以下四个角度考虑:
1、状态定义
2、状态之间的转移方程定义
3、状态的初始化
4、返回结果
状态定义的要求:定义的状态一定要形成递推关系
适用场景:
最值、可不可行、是不是、方案个数
思路:
状态F(i):第i项的值——要看这个第i项的值能不能返回到最后第n项的结果
返回结果:F(n)
状态转移方程:根据前面已经求解的状态结果,求解未知的状态结果,F(i)=F(i-1)+F(i-2)
初始状态:需要几个,看看初始状态方程需要几个项让这个方程运转下去:F(0)=0,F(1)=1
需要保存这些中间状态的解需要一个数组
本题是从0-n总共n+1项
class Solution {
public:
int Fibonacci(int n) {
int* F = new int[n + 1];
F[0] = 0;
F[1] = 1;
for (int i = 2; i <= n; ++i)
{
F[i] = F[i - 1] + F[i - 2];
}
return F[n];
}
};
接下来我们做一些优化,求当前第i位结果只需要前面两个状态的结果,其余结果是用不到的,空间进行了优化
class Solution
{
public:
int Fibonacci(int n)
{
if (n == 0)
return 0;
if (n == 1)
return 1;
int fn; // 第n项的结果
int fn1 = 1; // n - 1项
int fn2 = 0; // n - 2项
for (int i = 2; i <= n; ++i)
{
fn = fn1 + fn2;
fn2 = fn1; // 更新n-2
fn1 = fn; // 更新n-1项
}
return fn;
}
};
问题是字符串s能否被分割 -> 如何抽象状态?
状态定义:F(i)如何定义?——分解为相似的子问题
相似的一个子问题:某一个子串能不能被分割,也就是字符串的前i个字符是否可以被分割,然后看F(i)能不能对应到最终的解
状态转移方程:F(i)如何求?这里拿“leetcode”举例子,先试着分析F(4):前四个字符是否可以被分割——true;F(8):F(4)&&看看[5,8]是否可以在字典中能否被找到——true,因此这里最后可以返回F(8)
F(1) && [2,8]
F(2) && [3,8]
F(3) && [4,8] // F(3)的结果是前面递推出来的
F(4) && [5,8]
F(5) && [6,8]
F(6) && [7,8]
F(7) && [8,8]
F(i):j < i (前面状态小于当前状态)&& F(j)&& [j+1,i]是否可以在词典中找到
F(0):true
返回结果:F(字符串长度):f(s.size())
三角形顶部到底部的最小路径和
思路:从某一个位置(i,j)——>(i+1,j)和(i+1,j+1)就是移动到下一行相邻的位置
问题:从顶部到底部最小的路径和
状态F(i,j):从(0,0)到(i,j)的最小路径和
转移方程:F(i,j)=?,谁可以到达(i,j)这个点?(i - 1,j - 1)(i - 1,j)可以到达(i,j)点
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5100, INF = 1e9;
int f[N][N], a[N][N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= i; ++j)
{
scanf("%d", &a[i][j]);
}
}
for (int i = 0; i <= n; ++i)
{
for (int j = 0; j <= i + 1; ++j) // 注意此处是i + 1,防止边界问题,所以也将边界外的也初始化为-无穷
{
f[i][j] = -INF;
}
}
f[1][1] = a[1][1];
for (int i = 2; i <= n; ++i) // 从第二行开始
{
for (int j = 1; j <= i; ++j)
{
f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);
}
}
int ans = -INF;
for (int i = 1; i <= n; ++i)
{
ans = max(ans, f[n][i]);
}
cout << ans << endl;
return 0;
}
不同路径数目i
分析:
子问题就是到达任意一个点(i,j)的路径个数
状态定义F(i,j):从(0,0)到达(i,j)的路径个数
状态转移方程(某个状态可以只用一步到达这个状态,不能是多步到达):F(i,j):
class Solution {
public:
/**
*
* @param m int整型
* @param n int整型
* @return int整型
*/
int uniquePaths(int m, int n)
{
// write code here
vector<vector<int>> dp(m, vector<int>(n, 0)); // 初始化二维vector
for (int i = 0; i < m; ++i)
{
dp[i][0] = 1; // 第一行的方案数只有一个
}
for (int j = 0; j < n; ++j)
{
dp[0][j] = 1; // 第一列的方案数只有一个
}
for (int i = 1; i < m; ++i)
{
for (int j = 1; j < n; ++j)
{
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};
class Solution {
public:
/**
*
* @param grid int整型vector<vector<>>
* @return int整型
*/
int minPathSum(vector<vector<int> >& grid) {
// write code here
int row = grid.size();
int col = grid[0].size();
for (int i = 1; i < col; ++i)
{
grid[0][i] = grid[0][i - 1] + grid[0][i];
}
for (int j = 1; j < row; ++j)
{
grid[j][0] = grid[j - 1][0] + grid[j][0];
}
for (int i = 1; i < row; ++i)
{
for (int j = 1; j < col; ++j)
{
grid[i][j] = min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
}
}
return grid[row - 1][col - 1];
}
};
01背包问题
01背包:每个物品只有一个
总共四种情况:
价值大、体积小
价值大、体积大
价值小、体积大
价值小、体积小
状态F(i):从前i个商品中做选择, 包的最大值——>可以定位到最终问题的解,所以状态应该是可行的(做题时候的思维)
第i个商品放还是不放?——对应两种情况
放入第i个商品放入F(i):F(i - 1) + V[i - 1] // F中的i-1就表示前i - 1,V[i - 1]是因为下标从0开始——这里还是有问题的,没有考虑到体积的问题,需要考虑包内剩余空间大小是多少因此还需要进行修改:
因此进行修改:
状态索引的范围是从1开始,价值数组是从0开始
状态F(i,j):从前i个商品中选择,包的大小为j时,最大价值
状态转移方程:
能直接在剩余的位置放入:F(i,j):F(i-1,j)+V[i - 1]
需要把某些物品抠出来,在放入第i个:
F(i-1, j- A[i - 1])+V[i - 1]
因此要判断第i个物品要不要放入:
第i个商品可以放入大小为j的包中
F(i,j) = max(F(i - 1, j), F(i - 1, j - A[i - 1]) + V[i - 1])
第i个商品太大,大小为j的包放不下第i个商品
F(i,j) = F(i - 1,j)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int v[N], w[N]; // 体积和价值的数组
int f[N][N];
int main()
{
int n, m;
cin >> n >> m; // 物品数量和背包容量
for (int i = 1; i <= n; ++i) // 1~n 总共n个物品
{
cin >> v[i] >> w[i];
}
// 一开始由于f是全局,因此f[0][1~m] 前0个物品使得总体积不超过1~m的状态方程为0
for (int i = 1; i <= n; ++i)
{
for (int j = 0; j <= m; ++j)
{
// 划分两个集合,不包含第i个物品
f[i][j] = f[i - 1][j];
if (j >= v[i])
{
// 含第i个物品,在包含i和不包含i之间选择一个最大值
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
优化
转化为一维:
若j从小到大,f[j-v[i]]中,由于j-v[i]小于j,f[j-v[i]]已经在i这层循环被计算了,而我们想要的f[j-v[i]]应该是i-1层循环里面的,所以j从大到小的话保证此时的f[j-v[i]]还未被计算,也就是第i-1层的数据
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int v[N], w[N]; // 体积和价值的数组
int f[N];
int main()
{
int n, m;
cin >> n >> m; // 物品数量和背包容量
for (int i = 1; i <= n; ++i) // 1~n 总共n个物品
{
cin >> v[i] >> w[i];
}
// 一开始由于f是全局,因此f[0][1~m] 前0个物品使得总体积不超过1~m的状态方程为0
for (int i = 1; i <= n; ++i)
{
for (int j = m; j >= v[i]; --j) // if条件原本要满足j >= v[i],因此j范围在0~v[i - 1]是没有用的,可以直接不考虑
{
// 由于f[i]只用到了上一层f[i - 1],因此可以直接变为1维
/*
f[i][j] = f[i - 1][j]; -> f[j] = f[j];
*/
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m] << endl;
return 0;
}