动态规划专题详细总结(常见简单类型)

什么是动态规划

动态规划(Dynamic Programming, DP)是一种用来解决一类最优化问题的算法思想。简单来说,动态规划将一个复杂的问题分解为若干个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态规划会将每个求解过的子问题的解记录下来,这样当下一次碰到同样的子问题时,就可以直接使用之前记录的结果,而不是重复计算。注意:虽然动态规划采用这种方式来提高计算效率,但不能说这种做法就是动态规划的核心。

一般可以使用递归或者递推的写法来实现动态规划,其中递归的写法在此处又称作记忆化搜索

注意:一个问题必须具有重叠子问题最优子结构,才能使用动态规划去解决。

重叠子问题:如果一个问题可以被分解为若干个子问题,且这些子问题会重复出现,那么就称这个问题拥有重叠子问题。

最优子结构:如果一个问题的最优解可以由其子问题的最优解有效地构造出来,那么称这个问题拥有最优子结构。

一些概念的区别

分治与动态规划

分治和动态规划都是将问题分解为子问题,然后合并子问题的解得到原问题的解。但不同的是,分治法分解出的子问题是不重叠的,因此分治法解决的问题不拥有重叠子问题,而动态规划解决的问题拥有重叠子问题。

贪心与动态规划

贪心和动态规划都要求原问题必须拥有最优子结构。二者的区别在于,贪心法采用的计算方式类似于上面介绍的“自顶向下”,但是并不等待子问题求解完毕后再选择使用哪一个,而是通过一种策略直接选择一个子问题去求解,没被选择的子问题就不去求解了,直接抛弃。也就是说,它总是只在上一步选择的基础上继续选择,因此整个过程以一种单链的流水方式进行,显然这种所谓“最优选择”的正确性需要用归纳法证明。

一些小知识

字符串的字串和子序列的区别

对于一个字符串而言,比如:pikachu

字串:是在字符串中,取出一块(连续的),如:pik, ach, kac等

子序列:指的是从字符串中,顺序取出字符,但是可以不连续:如:pau, kch, icu等

无后效性

状态的无后效性是指:当前状态记录了历史信息,一旦当前状态确定,就不会改变,且未来的决策只能在已有的一个或若干个状态的基础上进行,历史信息只能通过已有的状态去影响未来的决策。

对于动态规划的问题来说,总会有很多设计状态的方式,但并不是所有的状态都具有无后效性,因此必须设计一个拥有无后效性的状态以及相应的状态转移方程,否则动态规划就没有办法得到正确结果。

最大连续子序列和

问题

给定一个数字序列 A 1 , A 2 , A 3 . . . , A n A_1,A_2,A_3...,A_n A1,A2,A3...,An,求i,j( 1 ≤ i ≤ j ≤ n 1 \leq i \leq j \leq n 1ijn)使得 A i + . . . + A j A_i+...+A_j Ai+...+Aj最大,输出这个最大和。

例如:-2 11 -4 13 -5 -2

显然11+(-4)+13=20 和为最大的选取情况,因此最大和为20

这个问题如果使用暴力法来做,枚举左端点和右端点需要O(n2)的复杂度,而计算A[i]+…+A[j]需要O(n)的复杂度,因此总复杂度为O(n3)。

就算采用记录前缀和的方法(预处理S[i] = A[0] + A[1] + … +A[i],这样A[i] + … +A[j] = S[j] - S[i-1])使用计算的时间变为O(1),总复杂度仍然有O(n2),这对n为105大小的题目来说是无法承受的。

动态规划

使用动态规划法时间复杂度仅有O(n)

令状态dp[i]表示以A[i]作为末尾的连续序列的最大和(这里是说A[i]必须作为连续序列的末尾)。以样例为例:-2 11 -4 13 -5 -2,下标分别记为0,1,2,3,4,5,那么

dp[0] = -2,

dp[11] = 11,

dp[2] = 7(11 + (-4) = 7),

dp[3] = 20(11 + (-4) + 13 = 20),

dp[4] = 15 (因为由dp数组的含义,A[4] = -5 必须作为连续序列的末尾,于是最大和就是 11 + (-4) + 13 + (-5) = 15,而不是20)

dp[5] = 13(11 + (-4) + 13 + (-5) + (-2) = 13)。

因为dp[i]要求是必须以A[i]结尾的连续序列,那么只有两种情况:

  • 这个最大和的连续序列只有一个元素,即以A[i]开始,以A[i]结尾。
  • 这个最大和的连续序列有多个元素,即从前面某处A[p]开始(p<i),一直到A[i]结尾。

对于第一种情况,最大和就是A[i]本身。

对于第二种情况,最大和就是dp[i-1]+A[i]

于是得到状态转移方程dp[i] = max{A[i], dp[i-1]+A[i]}

代码:

#include<stdio.h>
#define max(x, y) ((x)>(y)?(x):(y))
#define MAXN 20

int n = 6;
int a[] = {0, -2, 11, -4, 13, -5, -2};      //一般不用下标为0的元素
//求解结果表示
int dp[MAXN];

void maxSubSUm()
{
    dp[0] = 0;
    for(int i = 0;i<n;i++)
        dp[j] = max(dp[i - 1] + a[i], a[i]);
}

void dispmaxSum()
{
    int maxj = 1;
    for(int j = 2;j<=n;j++)
    {
        if(dp[j]>dp[maxj])
            maxj = j;
    }
    for(int k = maxj;k<=1;k--)
    {
        if(dp[k]<=0)
            break;
    }
    printf("最大连续子序列和:%d\n",dp[maxj]);
    printf("所选子序列:");
    for(int i = k+1;i<maxj;i++)
    {
        printf("%d",a[i]);
    }
    printf("\n");
}

int main()
{
    maxSubSum();
    printf("求解结果\n");
    dispmaxSum();
}

最长上升子序列(LIS)

问题

在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列是上升的。

例如,现有序列A = {1,2,3,-1,-2,7,9}(下标从1开始),它的最长不下降子序列是{1,2,3,7,9),长度为5。另外,还有一些子序列是上升子序列,比如{1,2,3}、{-2,7,9}等,但都不是最长的。

注意,也有些情况求的是最长非降序子序列,二者区别就是序列中是否可以有相等的数。假设我们有一个序列 b i,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们也可以从中得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N,但必须按照从前到后的顺序。比如,对于序列(1, 7, 3, 5, 9, 4, 8),我们就会得到一些上升的子序列,如(1, 7, 9), (3, 4, 8), (1, 3, 5, 8)等等,而这些子序列中最长的(如子序列(1, 3, 5, 8) ),它的长度为4,因此该序列的最长上升子序列长度为4。

对于这个问题如果我们使用最原始的枚举法,其时间复杂度为O(2n),这显然是不能承受的。

这里我们常使用的方法是三种:分别是O(n2)的DP,O(nlogn)的二分+贪心法,以及O(nlogn)的树状数组优化的DP。(本文详细介绍的是DP,有关该LIS的详细方法可以看这篇文章:最长上升子序列 (LIS) 详解+例题模板 (全)

动态规划

令dp[i]表示以A[i]结尾的最长上升子序列长度(和最大连续子序列和问题一样,以A[i]结尾是强制要求),这样对A[i]来说就会有两种可能:

  • 如果存在A[i]之前的元素A[j](J<i),使得A[j]<A[i](如果是最长不下降子序列:这里是A[j] ≤ \leq A[i],这里大家仔细分析一下就行,换汤不换药)且dp[j]+1>dp[i](即把A[i]跟在A[j]结尾的LIS后面时能比当前以A[i]结尾的LIS长度更长),那么就把A[i]跟在以A[j]结尾的LIS后面,形成一条更长的LIS(令dp[i] = dp[j] + 1)。
  • 如果A[i]之前的元素都比A[i]大,那么A[i]就只好自己形成一条LIS,但是长度为1,即这个子序列里面只有一个A[i]。

状态转移方程为:dp[i] = max{1, dp[j] + 1} (j = 1,2,…,i-1&&A[j]<A[i])

代码:

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100;
int A[N], dp[N];

int main()
{
    int n;
    scanf("%d", &n);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d", &A[i]);
    }
    
    int ans = -1;     //记录最长的dp[i]
    for(int i = 1;i <= n;i++)
    {
        dp[i] = 1;     //边界初始条件,即设每个元素自成一个子序列
        for(int j = 1;j < i;j++)
        {
            if(A[i] >= A[j] && (dp[j] + 1)>dp[i])
                dp[i] = dp[j] + 1;        //状态转移方程
        }
        ans = max(ans, dp[i]);
    }
    printf("%d", ans);
    return 0;
}

最长公共子序列(LCS)

问题

给定两个字符串(或数字序列)A和B,求一个字符串,使得这个字符串是A和B的最长公共部分(子序列可以不连续)。
在这里插入图片描述
如图所示,字符串“sadstorry”与“adminsorry”的最长公共子序列为“adsory”,长度为6。

动态规划

令dp[i][j]​表示字符串A的i号位和字符串B的j号位之前的LCS长度(下标从1开始),如dp[4][5]表示“sads”与“admin”的LCS长度。那么可以根据A[i]和B[j]的情况,分为两种决策:

  • 若A[i] == B[j],则字符串A与字符串B的LCS增加了一位,即有dp[i][j] = dp[i-1][j-1]+1。
  • 若A[i] != B[j],则字符串A的i号位和字符串B的j号位之前的LCS无法延长,因此dp[i][j]将会继承dp[i-1][j]与dp[i][j-1]中的较大值,即有dp[i][j] = max{dp[i-1][j], dp[i][j-1]}。

如下表所示:

jadminsorry
i012345678910
000000000000
s100000000000
a201111111111
d301222222222
s401222233333
t501222233333
o601222234444
r701222234555
y801222234556

由此可以得到状态转移方程:

d p [ i ] [ j ] = { d p [ i − 1 ] [ j − 1 ] + 1 , A [ i ] = = B [ j ] max ⁡ ( d p [ i − 1 ] [ j ] , d [ i ] [ j − 1 ] ) , A [ i ] ! = B [ j ] dp[i][j] = \begin{cases} dp[i-1][j-1]+1,A[i]==B[j] \\ \max(dp[i-1][j], d[i][j-1]),A[i]!=B[j] \end{cases} dp[i][j]={dp[i1][j1]+1,A[i]==B[j]max(dp[i1][j],d[i][j1]),A[i]!=B[j]

边界:dp[i][0] = dp[0][j] = 0 (0 ≤ \leq i ≤ \leq n, 0 ≤ \leq j ≤ \leq m)

这样状态dp[i][j]只与其之前的状态有关,由边界发出就可以得到整个dp数组,最终dp[n][m]就是需要的答案,时间复杂度为O(nm)

代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100;
char A[N], B[N];
int dp[N][N];

int main()
{
    int n;
    gets(A+1);   //从下标为1开始读入
    gets(B+1);
    int lenA = strlen(A + 1);     //由于读入时下标从1开始,因此读取长度也从+1开始
    int lenB = strlen(B + 1);
    //边界
    for(int i = 0; i<=lenA; i++)
    {
        dp[i][0] = 0;
    }
    for(int j = 0; j<lenB; j++)
    {
        dp[0][j] = 0;
    }
    //状态转移方程
    for(int i = 1; i<=lenA; i++)
    {
        for(int j = 1;j<=lenB; j++)
        {
            if(A[i]==B[j])
                dp[i][j] = dp[i-1][j-1]+1;
            else
            	dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
        }
    }
    //dp[lenA][lenB]是答案
    printf("%d\n",dp[lenA][lenB]);
    return 0;
}

最长回文子串

问题

给出一个字符串S,求S的最长回文子串的长度。

样例:字符串“PATZJUJZTACCBCC”的最长回文子串为“ATZJUJZTA”,长度为9。

如果使用暴力枚举法,从复杂度上来看,枚举端点需要O(n2),判断回文需要O(n),因此总的复杂度为O(n3)。

动态规划

使用动态规划法负责度为O(n2),最长回文子串有很多种使用动态规划的方法,这里仅仅介绍最容易理解的一种。

令dp[i][j]表示S[i]至S[j]所表示的子串是否是回文子串,是则为1,否则为0.这样根据S[i]是否等于S[j],可以把转移情况分为两类:

  • 若S[i] == S[j],那么只要S[i+1]至S[j-1]是回文子串,S[i]至S[j]就是回文子串;如果S[i+1]至S[j-1]不是回文子串,则S[i]至S[j]也不是回文子串。
  • 若S[i] != S[j],那么S[i]至S[j]一定不是回文子串。

状态转移方程:

d p [ i ] [ j ] = { d p [ i + 1 ] [ j − 1 ] , S [ i ] = = S [ j ] 0 , S [ i ] ! = S [ j ] dp[i][j] = \begin{cases} dp[i+1][j-1],S[i] == S[j] \\ 0,S[i] != S[j] \end{cases} dp[i][j]={dp[i+1][j1],S[i]==S[j]0,S[i]!=S[j]

边界:dp[i][i] = 1,dp[i][i+1] = (S[i] == S[i+1]) ? 1 : 0。

代码:

#include<iostream>
#include<cstring>
const int maxn = 1010;
char S[maxn];
int dp[maxn][maxn];

int main()
{
    gets(s);
    int len = strlen(S), ans = 1;
    memset(dp, 0, sizeof(dp));   //dp数组初始化
    //边界
    for(int i = 0; i<len; i++)
    {
        dp[i][i] = 1;     
        if(i < len - 1)
        {
            if(S[i] == S[i+1])
            {
                dp[i][i + 1] = 1;
                ans = 2;    //初始化时注意当前最长回文子串长度
            }
        }
    }
    //状态转移方程
    for(int L = 3; L<=len; L++)
    {
        for(int i = 0; i + L - 1<len; i++)
        {
            int j = i + L - 1;
            if(S[i] == S[j]&&dp[i + 1][j - 1] == 1)
            {
                dp[i][j] = 1;
                ans = L;     //更新最长回文子串长度
            }
        }
    }
    printf("%d\n", ans);
    return 0;
}

在这里还有二分+字符串hash的做法,复杂度O(nlogn)。不过,最优秀的当前复杂度为O(n)的Manacher算法。

DAG最长路

DAG是有向无环图,该问题可以使用“关键路径”求解,但是使用DP思想更为简便,在这里我们主要要解决两个问题:

  1. 求整个DAG中的最长路径(即不固定起点和终点)
  2. 固定终点,求DAG的最长路径。

问题1

**给定一个有向无环图,怎样求解整个图的所有最短路径中权值之和最大的那条。**如下图所示B -> D -> F -> I就是改图的最长路径,长度为9。
在这里插入图片描述

动态规划

针对这个问题,令dp[i]表示从i顶点出发能获得的最长路径长度,这样所有dp[i]的最大值就是整个DAG的最长路径长度。

那么怎样求解dp数组呢?注意到dp[i]表示从i号顶点出发能获得的最长路径的长度,如果从i号顶点出发能直接到达顶点j1、j2、…、jk,而dp[j1]、dp[j2]、…、dp[jk]均已知,那么就有dp[i] = max{dp[j] + length[i → \rightarrow j]|(i, j) ∈ \in E},如图所示:

显然根据上面的思路,需要按照逆拓扑序列的顺序来求解dp数组。但是有没有不求出逆拓扑序列也能计算dp数组的方法呢?当然有,那就是递归。请看下面的代码,其中图使用邻接矩阵的方式存储:

int Dp(int i)
{
    if(dp[i] > 0)
        return dp[i];    //dp[i]已计算得到
    for(int i = 0;j<n;j++)
    {
        if(G[i][j] != INF)
        {
            dp[i] = max(dp[i], DP(j) + G[i][j]);
        }
    }
    return dp[i];  //返回计算完比的dp[i]
}

由于从出度为0的顶点出发的最长路径的长度为0,因此边界为这些顶点的dp值为0,。但具体实现中不妨对整个dp数组初始化为0,这样dp函数当前方位的顶点i的出度为0时就会自动返dp[i] = 0(以此作为dp的边界),而出度不是0的顶点这会递归求解,递归过程中遇到已经计算过的顶点则直接返回对应的dp值。

问题2

这里我们讨论第二个问题:固定终点,求DAG的最长路径长度。

例如,还是那个图

在这里插入图片描述

如果我们固定H为路径终点,那么最长路径就会变为B → \rightarrow D → \rightarrow F → \rightarrow H。

有了上面的经验,应该能很容易想到这个延伸问题的解决方案。假设规定的终点为T,那么可以**令dp[i]表示从i号顶点出发到达终点T能获得的最长路径长度。**同样的,如果从i号顶点出发能直接到达顶点j1、j2、…、jk,而dp[j1]、dp[j2]、…、dp[jk]均已知,那么就有dp[i] = max{dp[j] + length[i → \rightarrow j]|(i, j) ∈ \in E}。

可以发现,这个式子和第一个问题的式子是一样的——但是如果仅仅是这样,显然无法体现出dp数组的含义中增加的“到达终点T”的描述。那么这两个问题的区别应该体现在哪里呢?没错,边界。在第一个问题中没有固定的终点,因此所有出度为0的顶点的dp值为0是边界;但是在这个问题中固定了终点,因此边界应当为dp[T] = 0。那么可不可以像之前的做法那样对整个dp数组都赋值为0呢?不行,此处会有一点问题。由于从某些顶点出发可能无法到达终点T(例如从出度为0的顶点),因此如果按之前的做法可能会得到错误的结果(例如出度为0的顶点会得到0),这从含义上说是不对的。合适的做法是初始化dp数组为一个负的大数,来保证“无法到达终点”的含义得以表达(即-INF);然后设置一个vis数组表示顶点是否已经被计算。

代码如下:

int Dp(int i)
{
    if(vis[i])
        return dp[i];    //dp[i]已计算得到
    vis[i] = true;
    for(int j = 0;j<n;j++)
    {
        if(G[i][j] != INF)
        {
            dp[i] = max(dp[i], Dp(j) + G[i][j]);
        }
    }
    return dp[i];
}

注意,有一些经典的问题可以转化为DAG的最长路,例如经典的矩形嵌套问题

背包问题

本文先介绍两类简单的背包问题:01背包问题和完全背包问题。

多阶段动态规划问题

有一类动态规划可解的问题,它可以描述成若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关,一般把这类问题称为多阶段动态规划问题。

如图所示 ,该问题被分为了五个阶段,其中状态F属于阶段3,它由阶段2的状态C和状态D推得。显然,对于这种问题,只需要从第一个问题开始,按照阶段的顺序解决每一个阶段中状态的计算,就可以得到最后一个阶段中的状态的解。这对设计状态的具体含义很有帮助,01背包就是这样的例子。

在这里插入图片描述

01背包问题

问题

有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包。问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有1件。

样例:

5 8 //n == 5, V == 8

3 5 1 2 2 //w[i]

4 5 2 1 3 //c[i]

如果采用暴力枚举每一件物品放或者不放进背包,显然每件物品都有两种选择,因此n件物品就有22种情况,而O(22)的复杂度显然是很糟糕的。而使用动态规划方法可以将复杂度降为O(nV)。

动态规划

令dp[i][v]表示前i件物品(1 ≤ \leq i ≤ \leq n, 0 ≤ \leq v ≤ \leq V)恰好装入容量为v的背包中所能获得的最大价值。怎么求解dp[i][v]呢?

考虑对第i件物品的选择策略,有两种策略:

  • 不放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,也即dp[i-1][v]。
  • 放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得最大价值,也即dp[i-1][v-w[i]] + c[i]。

由于只有这两种策略,且要求获得最大价值,因此状态转移方程:

d p [ i ] [ v ] = m a x ( d p [ i − 1 ] [ v ] , d p [ i − 1 ] [ v − w [ i ] ] + c [ i ] ) ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[i][v] = max(dp[i-1][v], dp[i-1][v-w[i]]+c[i]) (1\leq i \leq n, w[i] \leq v \leq V) dp[i][v]=max(dp[i1][v],dp[i1][vw[i]]+c[i])(1in,w[i]vV)

注意到dp[i][v]只与之前的状态dp[i-1][]有关,所以可以枚举i从1到n,v从0到V,通过边界dp[0][v] = 0(0 ≤ \leq v ≤ \leq V)(即前0件物品放入任何容量v的背包中都只能获得价值0)就可以把整个dp数组推出来。而由于dp[i][v]表示的恰好为v的情况,所以需要枚举dp[n][v](0 ≤ \leq v ≤ \leq V),取其最大值才是最后的结果。

代码:

for(int i = 1;i<=n;i++)
{
     for(int v = w[i];v<=V;v++)
     {
         dp[i][v] = max(dp[i-1][v], dp[i-1][v-w[i]]+c[i]);
     }
}

可以知道,时间复杂度和空间复杂度都是O(nV),其中时间复杂度已经无法再优化,但是空间复杂度还可再优化。

滚动数组

如下图所示,注意到状态转移方程中计算dp[i][v]时总是只需要dp[i-1][v]左侧部分的数据(即只需要图中正上方与左上方的数据),且当计算dp[i+1][]部分时,dp[i-1][]的数据又完全用不到了(只需要用到dp[i][]),因此不妨可以直接开一个一维数组dp[v](即把第一维省去了),枚举方向改变为i从1到n,v从V到0(逆序!),这样状态转移方程改变为:

d p [ v ] = m a x ( d p [ v ] , d p [ v − w [ i ] ] + c [ i ] ) ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[v] = max(dp[v], dp[v-w[i]] + c[i])(1\leq i\leq n, w[i]\leq v \leq V) dp[v]=max(dp[v],dp[vw[i]]+c[i])(1in,w[i]vV)

在这里插入图片描述

这样修改对应到图中,可以这样理解:v的枚举顺序从右往左,dp[i][v]右边部分为刚计算过的需要保存给下一行使用的数据,而dp[i][v]左上角的阴影部分为当前需要使用的部分。将这两者结合一下,即把dp[i][v]左上角和右边的部分放在一个数组里,每计算出一个dp[i][v],就相当于把dp[i-1][v]抹消掉,因为在后面的运算中dp[i-1][v]再也用不到了。我们把这种技巧称为滚动数组

代码如下:

for(int i = 1;i<=n;i++)
{
    for(int v = V;v>=w[i];v--)          //逆序枚举v
    {
        dp[v] = max(dp[v], dp[v-w[i]] + c[i]);
	}
}

这样01背包就可以用一维数组表示来解决了,空间复杂度为O(V)。

特别说明:如果是用二维数组存放,v的枚举是顺序还是逆序都无所谓;如果使用一维数组存放,则v的枚举必须是逆序!

完整代码:

#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn = 100;    //物品最大件数
const int maxv = 1000;   //V的上限
int w[maxn], c[maxn], dp[maxv];

int main()
{
    int n,V;
    scanf("%d%d", &n, &V);
    for(int i = 1;i<=n;i++)
    {
        scanf("%d", &w[i]);
    }
    for(int i = 1;i<=n;i++)
    {
        scanf("%d", &c[i]);
    }
    //边界
    for(int v = 0;v<=V;v++)
    {
        dp[v] = 0;
    }
    for(int i = 1;i<=n;i++)
    {
        for(int v = V;v>=w[i]; v--)
        {
            //状态转移方程
            dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
        }
    }
    //寻找dp[0...V]中最大的即为答案
    int max = 0;
    for(int v = 0;v<=V;v++)
    {
        if(dp[v]>max)
            max = dp[v];
    }
    printf("%d\n", max);
    return 0;
}

完全背包问题

问题

有n种物品,每种物品的单件重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都有无穷件。

可以看出,完全背包问题和01背包问题的唯一区别就在于:完全背包的物品数量每种有无穷件,选取物品时对同一种物品可以选择1件、2件…只要不超过容量V即可,而01背包的物品数量每种只有1件。

动态规划

同样,令dp[i][v]表示前i件物品恰好放入容量为v的背包中能获得的最大价值。

和01背包一样,完全背包问题的每种物品都有两种策略,但是也有不同点。对第i件物品来说:

  • 不放dii件物品,那么dp[i][v] = dp[i-1][v],这步跟01背包是一样的。
  • 放第i件物品。这里的处理和01背包有所不同,因为01背包的每件物品只能选择一个,因此选择放第i件物品就意味着必须转移到dp[i-1][v-w[i]]这个状态;但是完全背包不同,完全背包如果选择放第i件物品之后并不是转移到dp[i-1][v-w[i]],而是转移到dp[i][v-w[i]],这是因为每种物品可以放任意件(注意有容量的限制,因此还是有限的),放了第i件物品后还可以继续放第i件物品,直到第二维的v-w[i]无法保持大于等于0为止。

这样我们可以得到状态转移方程:

d p [ i ] [ v ] = m a x ( d p [ i − 1 ] [ v ] , d p [ i ] [ v − w [ i ] ] + c [ i ] ) ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[i][v] = max(dp[i-1][v], dp[i][v-w[i]] + c[i])(1\leq i\leq n,w[i] \leq v \leq V) dp[i][v]=max(dp[i1][v],dp[i][vw[i]]+c[i])(1in,w[i]vV)

边界:dp[0][v] = 0(0 ≤ \leq v ≤ \leq V)

看上去和01背包很像是不是?其实唯一的区别就在于max的第二个参数是dp[i]而不是dp[i-1]。而这个状态转移方程同样也可以改写成一维形式,即状态转移方程

d p [ v ] = m a x ( d p [ v ] , d p [ v − w [ i ] ] + c [ i ] ) ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[v] = max(dp[v], dp[v-w[i]] + c[i])(1\leq i \leq n, w[i] \leq v \leq V) dp[v]=max(dp[v],dp[vw[i]]+c[i])(1in,w[i]vV)

边界:dp[v] = 0(0 ≤ \leq v ≤ \leq V)

写成一维形式之后和01背包完全相同,唯一的区别在于这里v的枚举顺序是正向枚举,而01背包的一维形式中v必须是逆向枚举。完全背包的一维形式代码如下:

for(int i = 1;i<=n;i++)
{
    for(int v = w[i];v<=V;v++)    //正向枚举v
    {
        dp[v] = max(dp[v], dp[v-w[i]] + c[i]);
    }
}

怎么理解必须正向枚举呢?如下图所示,求解dp[i][v]需要它左边的dp[i][v-w[i]]和它上方的dp[i-1][v],显然如果让v从小到大枚举,dp[i][v-w[i]]就总是已经计算出的结果;而计算出dp[i][v]之后dp[i-1][v]就再也用不到了,可以直接覆盖。

在这里插入图片描述

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: ArcGIS打印专题图一般使用PDF类型。 PDF(Portable Document Format)是一种跨平台的文档格式,可以在不同操作系统和设备上保持文档的原格式和完整性。在ArcGIS中,专题图可以通过导出为PDF格式来进行打印。 使用PDF格式有以下几个优点: 1. 兼容性强:PDF格式可在不同操作系统和设备上打开和查看,无需担心兼容性问题。 2. 文件体积小:相比其他格式,PDF文件通常体积较小,方便传输分享。 3. 高质量输出:打印专题图时,PDF格式可以保持图像质量的高度还原,不会损失细节。 4. 文档完整性:PDF文件可以包含多个页面和图层,保持打印专题图时的图层关系和叠加效果。 5. 可编辑性:虽然PDF是一种静态格式,但也可以通过专业软件进行编辑,例如Adobe Acrobat Pro。 通过ArcGIS导出专题图为PDF格式,用户可以根据打印需求选择输出的页面尺寸、分辨率,并可以选择是否包含图例、比例尺、图层控制等元素。还可以设置打印区域、标注和注释等,以满足个性化的打印要求。 综上所述,ArcGIS打印专题图一般使用PDF格式,这种格式具有高质量输出、兼容性强、文件体积小等优点,可以满足用户对专题图打印的需求。 ### 回答2: ArcGIS打印专题图一般使用PDF格式或者JPEG格式。 在ArcGIS中,我们可以通过打印工具将地图以PDF或JPEG的形式输出为专题图。 PDF格式是一种开放标准的文档格式,可以保留地图的高质量和完整性,方便在不同平台和设备上查看和打印。PDF格式的专题图可以保留地图中的矢量数据和标签,使地图显示更清晰。 JPEG格式是一种常用的图像格式,也常用于打印专题图。JPEG格式的专题图可以将地图输出为一个图片文件,适用于需要将地图嵌入到文档或分享在网页上的场合。然而,JPEG格式是一种有损压缩的图像格式,会降低地图的细节和清晰度。 选择使用哪种类型专题图取决于具体的需求。如果需要保留地图的矢量数据和标签,同时保持高质量的输出,则选择PDF格式;如果只需要一个图像文件,方便嵌入到其他文档或分享在网页上,则选择JPEG格式。 总之,ArcGIS提供了多种输出专题图的格式,可以根据需求选择合适的类型。 ### 回答3: 在ArcGIS中,打印专题图可以使用多种类型来呈现,具体取决于用户的需求和数据类型。以下是一些常用的专题类型: 1. 分级色带图:通过将数据分成几个范围,并给每个范围赋予不同的颜色值来显示数据的分布情况。这种类型专题图适用于显示数量和密度。 2. 等值线图:基于数据的数值分布,通过连接具有相同数值的点来显示地理现象的等值分布。这种类型专题图适用于显示连续变量的分布情况,如高程和气温。 3. 符号图:通过为每个要素分配不同的符号或图标来表示它们的特点或属性。例如,可以使用不同的图标来表示不同类型的地理要素,如建筑物、河流和森林。 4. 点密度图:根据点要素的密度和聚集程度来表示区域的特征。这种类型专题图适用于显示人口分布、犯罪率和热点分析等。 5. 条带图:通过在地图上绘制并排的平行带来表示地理现象的分布情况。这种类型专题图适用于显示地理现象的变化趋势,如降雨量或温度的季节性变化。 根据实际需求,可以选择适用的专题类型来有效传达地理数据的信息和特征。ArcGIS提供了丰富的工具和选项,以供用户选择并创建满足需求的专题类型

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胡小涛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值