动态规划(Dynamic Programming)
动态规划是一种分阶段求解决策问题的数学思想。一般来说,只要问题可以划分为规模更小的字问题,并且原问题的最优解中包含了子问题的最优解,则可以考虑用动态规划解决
与贪心法的关系:
1.与贪心法类似,都是将问题实例归纳为更小的、相似的子问题,并通过求解子问题产生一个全局最优解。
2.贪心法选择当前最优解,而动态规划通过求解局部子问题的最优解来达到全局最优解。
与递归法的关系:
1.与递归法类似,都是将问题实例归纳为更小的、相似的子问题。
2.递归法需要对子问题进行重复计算,需要耗费更多的时间与空间,而动态规划对每个子问题只求解一次。对递归法进行优化,可以使用记忆化搜索的方式。它与递归法一样,都是自顶向下的解决问题,动态规划是自底向上的解决问题。
递归问题——>重叠子问题——> 1.记忆化搜索(自顶向上的解决问题);2.动态规划(自底向上的解决问题)
关于动态规划更详细的的讲解:https://blog.csdn.net/mmc2015/article/details/73558346
概念
(1)最优子结构 (2)边界 (3)状态转移公式
性质
最优子结构性质:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
子问题重叠性质:动态规划利用子问题的重叠性质,将需要重复计算的子问题结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
无后效性:对于任意阶段的状态,它以前各阶段的状态无法直接影响它未来的决策。换句话说,每个状态都是过去历史的一个完整总结。
基本解题步骤
(1)找出最优解的性质(最优子结构),并刻划其结构特征。
(2)递归地定义最优值。
(3)以自底向上的方式计算出最优值。
(4)根据计算最优值时得到的信息,写出状态转移方程。
下面是动态规划经典例题
01背包问题
/*--------------------------------------------------
有 N 件物品和一个容量为 V 的背包。
放入第 i 件物品耗费的空间是 w i ,得到的价值是 v i 。
求解将哪些物品装入背包可使价值总和最大。
要求恰好装满背包,在初始化时 dp[0] = 0 ,其它dp[1~V]均设为 −∞ 。
没有要求必须把背包装满,初始化时应该将 F[0~V] 全部设为 0 。
求方案数时,在初始化时F[0] = 1,其他F[1~V]均设为0。
--------------------------------------------------*/
int w[N], v[N], dp[V+1], V;
//w[]物品所占容量; v[]物品的价值; V为背包容量
int zeroOne() {
memset(dp, 0, sizeof(dp));
for(int i = 0; i < N; i++) { //第i件物品
for(int j = V; j >= w[i]; j--) { //填满空间j
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
return dp[V];
}
完全背包
/*--------------------------------------------------
有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。
放入第 i 种物品的耗费的空间是 w i ,得到的价值是 v i 。
求解:将哪些物品装入背包,可使这些物品的
耗费的空间总和不超过背包容量,且价值总和最大。
--------------------------------------------------*/
int w[N], v[N], dp[V+1], V;
int Complete() {
memset(dp, 0, sizeof(dp));
for(int i = 0; i < N; i++) {
for(int j = c[i]; j <= V; j++) {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
return dp[V];
}
多重背包
/*--------------------------------------------------
有 N 种物品和一个容量为 V 的背包。
第 i 种物品最多有 M i 件可用,每件耗费的空间是 w i ,价值是 v i 。
求解将哪些物品装入背包可使这些物品的
耗费的空间总和不超过背包容量,且价值总和最大。
二进制优化做法:
--------------------------------------------------*/
int w[a], v[a], m[a], dp[b], V;
//w[]:物品所占容量; v[]物品的价值; m[]物品的数量; V为背包容量
void ZeroOne(int w, int v) {//01背包
for(int i = V; i >= w; i--)
dp[i] = max(dp[i], dp[i-w]+v);
}
void Complete(int w, int v) {//完全背包
for(int i = w; i <= V; i++)
dp[i] = max(dp[i], dp[i-w]+v);
}
void Multiple(int w, int v, int m) {//多重背包
//如果总容量比这个物品的容量要小,那么这个物品可以直接取完,相当于完全背包
if(V <= m*w) {
Complete(w, v);
return;
} else { //否则就将多重背包转化为01背包
int k = 1;
while(k <= m) {
ZeroOne(k*w, k*v);
m -= k;
k <<= 1;
}
ZeroOne(m*w, m*v);
}
}
int main() {
// for(int i = 0; i <= V; i++) //初始化,不要求恰好装满背包
// dp[i] = 0;
for(int i = 0; i <= V; i++) //初始化:是否恰好装满背包
dp[i] = -0xffffff0;
dp[0] = 0;
for(int i = 0; i < N; i++)
Multiple(w[i], v[i], m[i]);
return 0;
}
混合背包
/*--------------------------------------------------
背包体积为V ,给出N个物品,每个物品占用体积为wi,价值为vi,
每个物品要么至多取1件,要么至多取mi件(mi > 1) ,
要么数量无限 , 在所装物品总体积不超过V的前提下
所装物品的价值的和的最大值是多少?
--------------------------------------------------*/
int V, n,w[a],v[a],m[a], dp[b];
for(int i=1; i<=n; i++) {
if(m[i] == -1) { //完全背包 -1表示无限取
for(int j=w[i]; j <= V; j++)
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
} else { //01与多重背包
for(int k=1; k <= m[i]; k++)
for(int j=V; j >= w[i]; j--)
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
二维费用背包
int w1[a], w2[a], v[a], dp[b][b], V1, V2;
memset(dp, 0, sizeof(dp));
int Costknapsack() {
for(int i = 0; i < N; i++) { //第i个
for(int j = w1[i]; j <= V1; j++) { //一维费用
for(int k = w2[i]; k <= V2; k++) { //二维费用
dp[j][k] = max(dp[j][k],dp[j-w1[i]][k-w2[i]] + v[i]);
}
}
}
return dp[V1][V2];
}
分组背包
/*--------------------------------------------------
有N件物品,告诉你这N件物品的重量以及价值,
将这些物品划分为K组,每组中的物品互相冲突,最多选一件,
求解将哪些物品装入背包可使这些物品的
费用综合不超过背包的容量,且价值总和最大。
--------------------------------------------------*/
int a[B][B];
int Groupingbackpack() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
for(i = 1; i <= n; i++) //第一重循环:分组数
for(j = V; j >= 0; j--) //第二重循环:容量体积
for(k = 0; k <= j; k++) //第三重循环:属于i组的k
dp[j] = max(dp[j], dp[j-k]+a[i][k]);
return dp[V];
}
K优解
int kth(int n, int V, int k) {
for (int i = 1; i <= n; i++) {
for (int j = V; j >= w[i]; j--) {
for (int l = 1; l <= k; l++) {
a[l] = dp[j][l];
b[l] = dp[j - w[i]][l] + v[i];
}
a[k + 1] = -1;
b[k + 1] = -1;
int x = 1, y = 1, o = 1;
while (o != k + 1 and (a[x] != -1 or b[y] != -1)) {
if (a[x] > b[y]) dp[j][o] = a[x], x++;
else dp[j][o] = b[y], y++;
if (dp[j][o] != f[j][o - 1]) o++;
}
}
}
return dp[V][k];
}
最长公共子序列长度
/*--------------------------------------------------
给定两个序列,找出在两个序列中同时出现的最长子序列的长度。
一个子序列是出现在相对顺序的序列,但不一定是连续的。
--------------------------------------------------*/
char s1[MAX_N],s2[MAX_M];
int dp[MAX_N+1][MAX_M+1];
int lcs(char *s1, char *s2) {
int len1 = strlen(s1);
int len2 = strlen(s2);
for(int i = 0; i < len1; i++) {
for(int j = 0; j < len2; j++) {
if(s1[i] == s2[j]) {
dp[i+1][j+1] = dp[i][j] + 1;
} else {
dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j]);
}
}
}
return dp[len1][len2];
}
最长递增子序列的长度O(NlogN)
int n;
int a[MAX_N], dp[MAX_N];
void lis() {
fill(dp, dp+n, INF);
for(int i=0; i < n; i++) {
*lower_bound(dp, dp+n, a[i]) = a[i];
}
printf("%d\n", lower_bound(dp, dp+n, INF) - dp);
}
最大连续子序列和
int n, a[n];
int MaxSubSequence() {
int ThisSum = 0,MaxSum = 0;
for(int i=0; i < n; i++) {
ThisSum += a[i];
if(ThisSum > MaxSum)
MaxSum = ThisSum;
else if(ThisSum < 0)
ThisSum = 0;
}
return MaxSum;
}
最大连续子矩阵和
/*==================================================*\
| 给你一个N,接下来是N*N的矩阵。数有正有负,求最大的子矩阵和。
\*==================================================*/
const int maxn=100;
int max_sub_matrix(int x[][maxn], int m, int n) {
// m和n分别代表行和列
int sum = -1000000; // 选择一个足够小的数
int p[maxn]; // 开辟一个用于存放和的一维数组
int dp[maxn];
for (int i = 0; i < m; ++i){
memset(p, 0, sizeof p);
for (int j = i; j < m; ++j){
for (int k = 0; k < n; ++k){
p[k] += x[j][k]; // p[k]为第k列,从第i行到第j行的和
}
// 以下for循环等价于 sum = max(sum, max_sub_array(p, n))
dp[0] = p[0];
for (int k = 1; k < n; ++k){
dp[k] = max(dp[k - 1] + p[k], p[k]);
sum = max(sum, dp[k]);
}
}
}
return sum;
}
最大M个连续子段的和
int dp[1000010];
int maxn[1000010];
int num[1000010];
int main() {
int M,N;
while(~scanf("%d%d",&M,&N)) {
dp[0] = maxn[0] = 0;
for(int i = 1; i <= N; i++)
{
scanf("%d",&num[i]);
dp[i] = maxn[i] = 0;
}
int MAXN;
for(int i = 1; i <= M; i++)//分为i段
{
MAXN = -0xffffff0;
for(int j = i; j <= N; j++)//第j个数字
{
dp[j] = max(dp[j-1]+num[j],maxn[j-1]+num[j]);
maxn[j-1] = MAXN;
MAXN = max(MAXN,dp[j]);
}
}
printf("%d\n",MAXN);
}
return 0;
}
最大不连续子序列和
/*==================================================*\
| 给你一个矩阵,不能选择每行中相邻的数字,也不能选
| 当前行的上一行和下一行,问使所选数和最大的值是多少?
| 对于每一行,都是求最大不连续子段和。
\*==================================================*/
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int MAXN = 200000;
int dpa[MAXN+20],dpb[MAXN+20],row[MAXN+20];
int main()
{
int M,N,num;
while(~scanf("%d%d",&M,&N))
{
memset(row,0,sizeof(row));
for(int i = 0; i < M; i++)
{
dpa[0] = dpb[0] = 0;
for(int j = 0; j < N; j++)
{
scanf("%d",&num);
dpa[j+1] = max(dpa[j],dpb[j]);// dp[j+1] 是到j为止,不吃j所能吃到的最大值
dpb[j+1] = dpa[j] + num;//吃j所能吃到的最大值
}
row[i] = max(dpa[N],dpb[N]);
}
dpa[0] = dpb[0] = 0;
for(int i = 0; i < M; i++)
{
dpa[i+1] = max(dpa[i],dpb[i]);
dpb[i+1] = dpa[i] + row[i];
}
int ans = max(dpa[M],dpb[M]);
printf("%d\n",ans);
}
return 0;
}
最长回文子序列
/*==================================================*\
| 给一个字符串,找出它的最长的回文子序列LPS的长度。
| 例如,如果给定的序列是“BBABCBCAB”,则输出应该是7,
| “BABCBAB”是在它的最长回文子序列。
\*==================================================*/
char s[MAX_N];
int dp[MAX_N][MAX_N]; //dp[i][j]表示s[i~j]最长回文子序列
int LPS(char *s) {
memset(dp,0,sizeof(dp));
int len = strlen(s);
for(int i = len-1; i >= 0; --i) {
dp[i][i] = 1;
for(int j = i+1; j < len; ++j) {
//头尾相同,最长回文子序列为去头尾的部分LPS加上头和尾
if(s[i] == s[j])
dp[i][j] = dp[i+1][j-1] + 2;
//头尾不同,最长回文子序列是去头部分的LPS和去尾部分LPS较长的
else
dp[i][j] = max(dp[i][j-1],dp[i+1][j]);
}
}
return dp[0][len-1];
}
最长回文子串
/*==================================================*\
| 给一个字符串,找出它的最长的回文子串(连续的串)的长度。
\*==================================================*/
string longestPalindrome(string s) {
const int n = s.size();
bool dp[n][n];
memset(dp, 0, sizeof(dp));
int maxlen = 1; //保存最长回文子串长度
int start = 0; //保存最长回文子串起点
for(int i = 0; i < n; ++i) {
for(int j = 0; j <= i; ++j) {
if(i - j < 2) {
dp[j][i] = (s[i] == s[j]);
} else {
dp[j][i] = (s[i] == s[j] && dp[j + 1][i - 1]);
}
if(dp[j][i] && maxlen < i - j + 1) {
maxlen = i - j + 1;
start = j;
}
}
}
return s.substr(start, maxlen);
}
切割钢条
/*==================================================*\
| 给定一段长度为n英寸的钢条和一个价格表Pi,求切割方案
| 使得销售收益Rn最大。
\*==================================================*/
//自底向上法
int p[110],r[110]; // r[n]来保存子问题
int BOTTOM_UP_CUT_ROD(int n) {
r[0] = 0; //长度为0的钢条没有收益
// 对i=1,2,3,…,n按升序求解每个规模为i的子问题。
for(int i = 1; i <= n; i++) {
int q = -INF;
for(int j = 1; j <= i; j++) {
// 直接访问数组r[i-j]来获得规模为j-i的子问题的解
q = max(q, p[j]+r[i-j]);
}
r[i] = q;
}
return r[n];
}
状压dp
/*==================================================*\
| 问题引入:在 n*n(n≤20)的方格棋盘上放置 n 个棋子
| 要求每行每列只放一个,求使它们不能互相攻击的方案总数
\*==================================================*/
int dp[1<<21];
int main() {
int n, temp;
scanf("%d", &n);
dp[0] = 1;//边界条件
for(int s = 1; s <= (1<<n)-1; s++) {
dp[s] = 0;
for(int i = 1; i <= n; i++)
if(s & (1<<(i-1))) { //排除掉第i行所有不能放置的位置之后的可放位置
temp = s ^ (1<<(i-1)); //可以得到s状态的状态
dp[s] += dp[temp]; //s状态下的方案数
}
}
printf("%d", dp[(1<<n)-1]);
return 0;
}
区间dp
/*==================================================*\
| 区间dp就是在区间上进行动态规划,求解一段区间上的最优解。
| 主要是通过合并小区间的 最优解进而得出整个大区间上最优解的dp算法。
\*==================================================*/
for(int len = 1;len<=n; len++) { //枚举长度
for(int j = 1;j+len<=n+1; j++) { //枚举起点,ends<=n
int ends = j+len - 1;
for(int i = j;i<ends; i++) { //枚举分割点,更新小区间最优解
dp[j][ends] = min(dp[j][ends], dp[j][i]+dp[i+1][ends]+something);
}
}
}
集合DP
/*==================================================*\
| 对于空间里有n个点,给定n个点间的距离,将n个点两两配成n/2对,
| 使得所有点对距离之和最小(n<=20)
\*==================================================*/
for(int S=0;S<(1<<n);S++) {
dp[S] = 0x3f3f3f3f;
for(int i=0; i < n; i++) {
if((1<<i) & S) break;
for(int j=0; j<n; j++) if((1<<j) & S) {
dp[S] = min(dp[S], dp[S^(1<<i)^(1<<j)]+dist[i][j]);
}
}
}
代码多源于网络,更多模板