线性动态规划是动态规划中最基础的一类问题

引入部分就先省略了,重要的是多刷题找到感觉

基本步骤

  • 状态表示:定义 $f[i]$ 表示前 $i$ 项的最优解(最大值、最小值、方案数等)

  • 状态转移:通过前面的状态推出当前状态,写出转移方程

只要正确的完成这两步,就可以快速求解了

空间优化

我们根据状态转移方程,发现在递推的过程中,只需要前面几个量就可以往下推,所以可以节省空间,这在数据量比较大的题中会很有用

最长上升子序列(LIS)

给定一个长度为 $n$ 的数组 $a$,求最长的严格递增子序列长度.

我们令$f[i]$ 表示以 $a[i]$ 结尾的最长上升子序列长度, $f[i]$ 表示以 $a[i]$ 结尾的 LIS 长度

那么状态转移方程就为

$$f[i] = \max(f[j] + 1),\quad \text{对所有 } j < i \text{ 且 } a[j] < a[i]$$

推导过程

我们定义了 $f[i]$ 是所有满足“最后一个元素是 $a[i]$”的严格递增子序列中,长度的最大值,现在无需考虑整个数组,我们只需要看以 $a[i]$ 结尾的子序列

要构造以 $a[i]$ 结尾的上升子序列,可以从前面任意一个 $a[j]$($j<i$)为结尾的上升子序列接过来,但是前提是 $a[j] < a[i]$ ,才能保持递增

所以有

$$f[i] = \max(f[j] + 1),\quad \text{对所有 } j < i \text{ 且 } a[j] < a[i]$$

简单吧

如果你看上去有点小蒙,别急,证明一下

简单证明

设存在若干个 $j<i$ 且 $a[j]<a[i]$ ,我们考虑所有这些 $j$ :

$$f[i] = \max_{j<i,\ a[j]<a[i]} \left\{ f[j] + 1 \right\}$$

如果一个位置 $j$ 不满足 $a[j] < a[i]$ ,那它不能接到 $a[i]$ 前面,所以不能构成上升子序列,也就不能参与转移

值得注意的是初始化要所有 $f[i]=1$,因为每个元素自身可以组成一个长度为 $1$ 的子序列

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5;
int n,a[MAXN],f[MAXN];
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i],f[i]=1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<i;j++)
            if(a[j]<a[i])
                f[i]=max(f[i],f[j]+1);
    cout<<*max_element(f+1,f+n+1);
}

这里有个实用的小技巧,max_element(begin,end) 可以返回指向区间中最大元素的迭代器(可以理解为指针),这里前面的* 解引用该迭代器

此时的复杂度: $O(n^2)$

贪心 + 二分优化

我们本质上是求LIS的长度而不是内容

所以可以维护一个数组 g[],表示当前长度为 $i$ 的递增子序列的最小结尾元素,这样每次找到结尾最小的元素就可以保证后面接上尽可能多的元素

思路:

  • 遍历每个 $a[i]$

  • g 中用 lower_bound 找第一个 ≥$a[i]$ 的位置 pos :

    • 若找不到($a[i]$ > 所有 $g[j]$),则 $g$ 末尾加一项

    • 否则替换 $g[pos]$ 为 $a[i]$

  • 最终 g.size() 就是答案.

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5;
int n,a[MAXN],g[MAXN],len;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++){
        int p=lower_bound(g+1,g+1+len,a[i])-g;
        g[p]=a[i];
        if(p>len)len=p;
    }
    cout<<len;
}

检验

https://www.luogu.com.cn/problem/B3637

最长公共子序列(LCS)

给定两个字符串 AB,求它们的最长公共子序列的长度(不要求连续,但要顺序一致)

如果觉得抽象可以这样理解

例如:
A = "abcde"
B = "ace"
最长公共子序列是 "ace",长度为 3 ,但是"aec" "eac" "eca" "cea""cae" 都不对

我们令
$A$ 长度为 $n$
$B$ 长度为 $m$
$f[i][j]$ 表示 $A$ 的前 $i$ 个字符 与 $B$ 的前 $j$ 个字符 的最长公共子序列长度

于是我们有
$$f[i][j] =
\begin{cases}
f[i-1][j-1] + 1 & \text{若 } A[i] = B[j] \\
\max(f[i-1][j],\ f[i][j-1]) & \text{否则}
\end{cases}$$

直观理解就是如果当前字符相同,就可以在原来基础上加一,否则只能继承上一个状态的最大值

简易理解

状态转移有两种情况

情况一:A[i] == B[j]

  • 当前两个字符相同,则它们可以作为当前公共子序列的最后一位

  • 所以 f[i][j] = f[i-1][j-1] + 1

  • 举例:

    • A: a b c d

    • B: a e c d

    • i=4, j=4,A[4]=B[4]=d

    • 那么以 d 结尾的 LCS 就是 f[3][3]+1

情况二:A[i] != B[j]

  • 当前字符不相等,不能同时选.

  • 此时最长公共子序列可能:

    • 不包含 A[i],即看 f[i-1][j]

    • 或不包含 B[j],即看 f[i][j-1]

  • 取最大值:f[i][j] = max(f[i-1][j], f[i][j-1])

样例示例

a = "abcde" b = "ace"

我们构建一个 $f[i][j]$ 表示 $a$ 的前 4i$ 个字符和 $b$ 的前 $j$ 个字符的 LCS 长度.

这是操作流程

$i$

$j$

$a[i-1]$

$b[j-1]$

是否相等

状态转移方式

f[i][j]

1

1

a

a

✅ 相等

f[0][0] + 1

1

1

2

a

c

❌ 不等

max(f[0][2], f[1][1]) = max(0,1)

1

1

3

a

e

❌ 不等

max(f[0][3], f[1][2]) = max(0,1)

1

2

1

b

a

❌ 不等

max(f[1][1], f[2][0]) = max(1,0)

1

2

2

b

c

❌ 不等

max(f[1][2], f[2][1]) = max(1,1)

1

2

3

b

e

❌ 不等

max(f[1][3], f[2][2]) = max(1,1)

1

3

1

c

a

❌ 不等

max(f[2][1], f[3][0]) = max(1,0)

1

3

2

c

c

✅ 相等

f[2][1] + 1 = 1 + 1

2

3

3

c

e

❌ 不等

max(f[2][3], f[3][2]) = max(1,2)

2

4

1

d

a

❌ 不等

max(f[3][1], f[4][0]) = max(1,0)

1

4

2

d

c

❌ 不等

max(f[3][2], f[4][1]) = max(2,1)

2

4

3

d

e

❌ 不等

max(f[3][3], f[4][2]) = max(2,2)

2

5

1

e

a

❌ 不等

max(f[4][1], f[5][0]) = max(1,0)

1

5

2

e

c

❌ 不等

max(f[4][2], f[5][1]) = max(2,1)

2

5

3

e

e

✅ 相等

f[4][2] + 1 = 2 + 1

3

#include<bits/stdc++.h>
using namespace std;
const int N = 1005;
int f[N][N];
string a, b;
int main(){
    cin >> a >> b;
    int n = a.size(), m = b.size();
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(a[i-1]==b[j-1]) f[i][j] = f[i-1][j-1]+1;
            else f[i][j] = max(f[i-1][j], f[i][j-1]);
        }
    }
    cout << f[n][m] << endl;
}

一维优化

#include<bits/stdc++.h>
using namespace std;
const int N=5005;
int f[2][N];
string a,b;
int main(){
	cin>>a>>b;
	int n=a.size(),m=b.size();
	int t=0;
	for(int i=1;i<=n;i++){
		t^=1;
		for(int j=1;j<=m;j++){
			if(a[i-1]==b[j-1]) f[t][j]=f[t^1][j-1]+1;
			else{
				if(f[t^1][j]>f[t][j-1]) f[t][j]=f[t^1][j];
				else f[t][j]=f[t][j-1];
			}
		}
	}
	cout<<f[t][m]<<endl;
	return 0;
}

检验

https://www.luogu.com.cn/problem/U197280

做完了挑战一下这个

https://www.luogu.com.cn/problem/P1439

背包问题

01背包

设想有这样一个问题

有一个容量为 $V$ 的背包,还有 $n$ 个物体.现在忽略物体实际几何形状,我们认为只要背包的剩余容量大于等于物体体积,那就可以装进背包里.每个物体都有两个属性,即体积 $w$ 和价值 $v$ .每种物品只能选 $0$ 次或 $1$ 次如何选择物品,使得在不超过背包容量的前提下,总价值最大?

我们设第 $i$ 个物品的体积和价值分别为 $w_i,v_i$ ,对于每一个物品,我们都可以进行两个操作,那就是选与不选

如果我们令 $f[i][j]$ 表示表示前 $i$ 个物品中选出若干个放入容量为 $j$ 的背包中的最大价值
如果不选某个物品,则 $f[i][j] = f[i-1][j]$
如果容量足够且选择这个物品,则 $f[i][j] = f[i-1][j-w[i]] + v[i]$

于是状态转移方程

$$f[i][j] = \max(f[i-1][j],\ f[i-1][j-w[i]] + v[i])$$

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 1005;
int n, C;
int w[MAXN], v[MAXN];
int f[MAXN][MAXN]; // f[i][j] 表示前 i 个物品中,背包容量为 j 时的最大价值

int main(){
    cin >> n >> C;//C是背包容量
    for(int i = 1; i <= n; i++) cin >> w[i] >> v[i]; // 输入物品的重量和价值

    for(int i = 1; i <= n; i++){ // 遍历所有物品
        for(int j = 0; j <= C; j++){ // 遍历所有可能的背包容量
            // 不选当前物品
            f[i][j] = f[i-1][j];
            // 如果可以放入当前物品
            if(j >= w[i]){
                f[i][j] = max(f[i][j], f[i-1][j-w[i]] + v[i]);
            }
        }
    }

    // 输出最大价值
    cout << f[n][C] << endl;
    return 0;
}

一维优化

维数组在数据量大的情况下容易爆空间

我们发现 f[i][j] 只依行的值,即 f[i-1][j]f[i-1][j - w[i]]

所以我们在处理第 i 个物品时,其实只需要用到 前一行的数据

我们引入一个一维数组 f[j] 来代替 f[i][j],关键在于必须从大到小遍历背包容量 j(倒序)

这是因为我们要模拟 f[i][j] = max(f[i-1][j], f[i-1][j-w[i]] + v[i]),而 f[j] 要代替 f[i][j],我们必须保证 在更新 f[j] 时,f[j - w[i]] 仍然是上一轮(即第 i-1 件物品)留下的值,而不是当前轮刚更新过的值。

当你处理第 i 个物品时,如果你用一维数组从小到大更新(例如 for(j = w[i]; j <= C; j++)

f[j] = max(f[j], f[j - w[i]] + v[i]);

那么 f[j - w[i]] 可能已经被本轮第 i 件物品更新过了 —— 这就相当于在同一轮里“用了两次同一个物品”,这就不是 01 背包了,而是完全背包

for(int i = 1; i <= n; i++){
    for(int j = C; j >= w[i]; j--){ // 倒序
        f[j] = max(f[j], f[j - w[i]] + v[i]);
    }
}

这样可以确保f[j - w[i]] 是更新前的旧值(等价于 f[i-1][j - w[i]]),f[j] 是第 i-1 件物品的最优解,现在考虑是否加入第 i

完全背包

与之前不同,完全背包可以对每个物品进行"多选",

我们前面说了,把01背包的第二层循环改成正序就变成了完全背包的解法,为什么是这样的呢,我们不妨先设f数组是二维的

$f[i][j]$表示表示前 $i$ 个物品中选出若干个放入容量为 $j$ 的背包中的最大价值,那么可以得出

$$f[i][j]=\max\Big(f[i-1][j],\ f[i][j-w_i]+v_i\Big)\quad(j\ge w_i)$$

它和01背包一样只依赖于上一层状态,不过我们无需倒序遍历,因为我们可以对物品多选,正序遍历可以在原来的刚才基础上继续选择最优解

一维优化

如果用滚动数组,那么可以写成这样

$$dp[j]=\max\big(dp[j],\ dp[j-w_i]+v_i\big)\quad(j\ge w_i,\ j\uparrow)$$

代码

无一维优化

#include<bits/stdc++.h>
using namespace std;
int n,V,w[10005],v[10005];
int f[1005][10005];
int main(){
    cin>>n>>V;
    for(int i=1;i<=n;i++)cin>>w[i]>>v[i];
    for(int i=1;i<=n;i++){
        for(int j=0;j<=V;j++)f[i][j]=f[i-1][j];
        for(int j=w[i];j<=V;j++)f[i][j]=max(f[i][j],f[i][j-w[i]]+v[i]);
    }
    cout<<f[n][V]<<endl;
}

一维优化

#include<bits/stdc++.h>
using namespace std;
int n,V,w[10005],v[10005],dp[1000005];
int main(){
    cin>>n>>V;
    for(int i=1;i<=n;i++)cin>>w[i]>>v[i];
    for(int i=1;i<=n;i++)
        for(int j=w[i];j<=V;j++)
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
    cout<<dp[V]<<endl;
}

多重背包

多重背包在完全背包原有的基础上发生了变异,现在你不可以无限地选择物品,每个物品的数量是有限的

我们还是从最简单的状态入手,还是每个物品选或不选,只是选择变成了选几个(变相的一种枚举)

每个物品 $i$ 有重量 $w_i$、价值 $v_i$、数量限制 $m_i$

$$f[i][j] = \text{前 } i \text{ 个物品中选若干个放入容量为 } j \text{ 的背包的最大价值}$$

那么多重背包的状态转移就是

$$f[i][j] = \max_{0 \le k \le m_i,\ k w_i \le j} \Big( f[i-1][j - k w_i] + k v_i \Big)$$

我们枚举第 $i$ 个物品取 $k$ 件,并且保证总重量不超过容量 $j$

我们发现它还是只依赖于前面的状态,于是进行一维优化

这种朴素的写法代码如下,复杂度较高

for(int i=1;i<=n;i++)
    for(int j=V;j>=0;j--)
        for(int k=1;k<=m[i] && k*w[i]<=j;k++)
            dp[j] = max(dp[j], dp[j-k*w[i]] + k*v[i]);

二进制拆分优化

我们发现数量 $m_i$ 大时,可以把它拆成若干个 01 背包物品

思路:

把第 $i$ 件、数量 $m_i$ 的物品拆成若干件 01 物品,件数为二进制块:$1,2,4,\dots, m_i-\text{sum}$。每块的 $(w,v)$ 乘以块大小。随后做 01 背包倒序转移

理解就是把每个“数量受限”的物品拆成若干个“块”(1,2,4,…,余数),每块就是一件0-1 物品,然后用0-1 背包倒序做即可

更深层次的原理就是任意一个非负整数 $m_i$,都能表示成若干个 不重复的二进制权值 之和

为了不弄混,我们先不进行一维优化

那么

$$f[i][j]=\max_{k\in\{0,1\}}f[i-1][j],\ f[i-1][j-w_i\cdot s]+v_i\cdot s,\ \text{依次合并各块}$$

于是发现第 $i$ 层的状态还是与 $i-1$ 层状态有关,我们进行一维优化就可以得到代码

#include<bits/stdc++.h>
using namespace std;
int n,V;
vector<int>w,v,m,nw,nv;
int main(){
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>n>>V;w.resize(n);v.resize(n);m.resize(n);
    for(int i=0;i<n;i++)cin>>w[i]>>v[i]>>m[i];
    for(int i=0;i<n;i++){
        int c=m[i],p=1;
        while(p<=c){nw.push_back(w[i]*p);nv.push_back(v[i]*p);c-=p;p<<=1;}
        if(c)nw.push_back(w[i]*c),nv.push_back(v[i]*c);
    }
    vector<long long>dp(V+1);
    for(int i=0;i<(int)nw.size();i++)
        for(int j=V;j>=nw[i];j--)
            dp[j]=max(dp[j],dp[j-nw[i]]+nv[i]);
    cout<<dp[V]<<'\n';
}

解释/证明

这部分确实不好理解,让我们证明一下

定理1:任意整数的“二进制拆分”是正确且唯一的

我们要证明任意非负整数

$$m=\sum_{k=0}^{t} b_k 2^k,\quad b_k\in\{0,1\}$$

是正确的且唯一的

存在性证明

设 $2^t\le m<2^{t+1}$ ,余数 $r=m-2^t\in[0,2^t)$ 若 $r=0$ 结束;否则对 $r$ 重复相同过程:
取不超过当前余数的最大的 $2$ 的幂

有限终止证明:因为在循环过程中每一步余数严格减小且非负,最多进行 $t+1$ 步,必定结束
互异性证明:每次选取的幂指数严格递减(先取 $t$ ,再取 $<t$ 的数),故所有的幂不重复
正确性:将各步所取的幂相加恰得原值,因为如果 $m$ 没有被减完,则不符合 $r=0$ 的结束条件

唯一性证明

假设存在两种不同的表示方式

$$m=\sum_{k=0}^{T} b_k2^k=\sum_{k=0}^{T} c_k2^k,\qquad b_k,c_k\in\{0,1\}$$

取最大的下标 $r$ 使 $b_r\ne c_r$ ,于是对所有 $k>r$ 都有 $b_k=c_k$,两种相减得

$$(c_r-b_r)2^r=\sum_{k=0}^{r-1}(b_k-c_k)2^k$$

把 $k>r$ 的项(它们系数为 0)去掉,只剩

$$(b_r-c_r)2^r \;+\; \sum_{k=0}^{r-1} (b_k-c_k)2^k \;=\; 0$$

且等价于

$$(b_r-c_r)2^r \;=\; -\,\sum_{k=0}^{r-1} (b_k-c_k)2^k.$$

对两侧取绝对值

$$\big|(b_r-c_r)2^r\big| \;=\; \bigg|\sum_{k=0}^{r-1} (b_k-c_k)2^k\bigg|$$

对于等式左侧:由于 $b_r,c_r\in\{0,1\}$ 且 $b_r\ne c_r$,所以 $b_r-c_r\in\{1,-1\}$ 因此

$$\big|(b_r-c_r)2^r\big| \;=\; 2^r$$

对于等式右边每一项都有 $|b_k-c_k|\le 1$ 利用三角不等式,

$$\bigg|\sum_{k=0}^{r-1} (b_k-c_k)2^k\bigg|
\;\le\; \sum_{k=0}^{r-1} |b_k-c_k|\,2^k
\;\le\; \sum_{k=0}^{r-1} 1\cdot 2^k
\;=\; 2^r-1$$

把两边的界拼在一起,得到

$$2^r \;=\; \big|(b_r-c_r)2^r\big|
\;=\; \bigg|\sum_{k=0}^{r-1} (b_k-c_k)2^k\bigg|
\;\le\; 2^r-1$$

这显然是不成立的,因此假设错误,必须有 $b_k=c_k$ 对所有 $k$ 都成立,唯一性得证

定理2:用这些块替代原物品,不会改变最优解

设原多重背包问题中有第 $i$ 类物品,重量 $w_i$、价值 $v_i$、数量上限 $m_i$。将其进行二进制拆分:

$$m_i = 2^0+2^1+\cdots+2^{t-1}+r,\quad 0\le r<2^t$$

并用以下 0-1 物品块替代该类物品:

$$(2^0w_i,\,2^0v_i),\ (2^1w_i,\,2^1v_i),\ \dots,\ (2^{t-1}w_i,\,2^{t-1}v_i),\ \text{以及(若 }r>0)\ (r w_i,\, r v_i)$$

记将所有物品都如此替代后的 01 背包实例为“拆分实例”.命题:原问题与拆分实例的**可行解集合一一对应,且对应解的总价值相同**,因此两者最优值相同

存在性证明

对原问题任一可行解,记第 $i$ 类实际取件数为 $k_i$,满足 $0\le k_i\le m_i$

由定理1,$k_i$ 存在唯一的表示

$$k_i=\sum_{s=0}^{t-1} b_{i,s}\,2^s + b_{i,t}\,r,\ \ \ \ b_{i,s}\in\{0,1\}$$

对拆分实例,在第 $i$ 类对应的各块中,令那些 $b_{i,s}=1$ 的块各“取 1 次”,其余块“取 0 次”,于是第 $i$ 类的总“等效件数”为

$$\sum_{s=0}^{t-1} b_{i,s}\,2^s + b_{i,t}\,r = k_i$$

其总重量与总价值分别为

$$k_i w_i,\quad k_i v_i$$

与原问题在该类物品取 $k_i$ 件完全一致.对每一类物品都如此构造,即得拆分实例的一个可行解.故原问题的任一可行解在拆分实例中都有对应解(不丢解)

唯一性证明

在拆分实例中,每个块是 0-1 物品,最多取 1 次。设在第 $i$ 类的块集合中选取了某个子集,令其等效件数为

$$\tilde k_i=\sum_{s=0}^{t-1} b_{i,s}\,2^s + b_{i,t}\,r,\qquad b_{i,s}\in\{0,1\}.$$

由于各块的“件数权值”互不相同($2^s$ 与 $r<2^t$),且每块至多选一次,故 $\tilde k_i$ 的二进制表示惟一,必有 $0\le \tilde k_i\le m_i$。于是对原问题取件 $\tilde k_i$ 件是合法的;其总重量与总价值为

$$\tilde k_i w_i,\quad \tilde k_i v_i,$$

与拆分实例中该类被选块的重量与价值之和相同。对所有类别并行执行,可将拆分实例的任意可行解还原为原问题的一个可行解。故拆分不会引入原问题中不存在的解

单调队列优化

我们发现可以把转移改写成“滑动窗口最大值”

多重背包一类物品 $(w,v,c)$ 的标准转移:

$$f'(j)=\max_{0\le t\le c,\;tw\le j}\bigl(f(j-tw)+t\,v\bigr)$$

按模 $w$ 分组,固定余数 $r\in[0,w-1]$,写

$$j=r+k\,w,\quad g_k=f(r+k\,w)$$

令 $s=k-t$(等价于 $t=k-s$),且 $t\in[0,c]\Rightarrow s\in[k-c,k]$则

$$g'_k=\max_{s\in[k-c,k]}\bigl(g_s+(k-s)\,v\bigr)
=k\,v+\max_{s\in[k-c,k]}\underbrace{\bigl(g_s-s\,v\bigr)}_{h_s}$$

对固定的 $r$,当 $k$ 从 $0,1,2,\dots$ 递增时,要求的仅是区间

$$[k-c,k]\ \text{上}\ h_s\ \text{的最大值}$$

这正是长度为 $c+1$ 的滑动窗口最大值问题

代码非常不好写,就先不详细记录了

#include <bits/stdc++.h>
using namespace std;
int main(){
    ios::sync_with_stdio(0);cin.tie(0);
    int n,V;if(!(cin>>n>>V))return 0;
    vector<int>w(n),v(n),c(n);
    for(int i=0;i<n;i++)cin>>w[i]>>v[i]>>c[i];
    const long long INF=1e18;
    vector<long long> f(V+1,-INF);f[0]=0;
    for(int i=0;i<n;i++){
        vector<long long> pre=f;
        int ww=w[i],vv=v[i],cc=c[i];
        for(int r=0;r<ww;r++){
            deque<int> q;
            for(long long k=0;;k++){
                long long j=r+k*ww; if(j>V)break;
                long long key=pre[j]-k*vv;
                while(!q.empty()){
                    long long s=q.back();
                    long long pj=r+1ll*s*ww;
                    if(pre[pj]-s*vv<=key)q.pop_back();else break;
                }
                q.push_back((int)k);
                while(!q.empty()&&q.front()<(int)(k-cc))q.pop_front();
                long long s=q.front(),pj=r+1ll*s*ww;
                f[j]=k*vv+pre[pj]-s*vv;
            }
        }
    }
    long long ans=*max_element(f.begin(),f.end());
    if(ans<-INF/2)cout<<"-inf\n";else cout<<ans<<"\n";
    return 0;
}

总结

类别 名称 转移/实现要点(极简) 时间复杂度 空间复杂度 备注
0/1 背包 朴素二维 $f[i][j]=\max(f[i-1][j],f[i-1][j-w_i]+v_i)$ $O(nV)$ $O(nV)$ 好理解,空间大
0/1 背包 一维优化 一维倒序 $j=V\to w_i$ 更新 $O(nV)$ $O(V)$ 读旧层、写新层同数组,靠倒序避免串层
完全背包 朴素枚举件数(二维) $f[i][j]=\max_{k\ge0} f[i-1][j-k w_i]+k v_i$ 最坏 $O(nV^2)$* $O(nV)$ 当 $w_i=1$ 退化为 $O(nV^2)$
完全背包 一维优化 一维正序 $j=w_i\to V$ 更新 $O(nV)$ $O(V)$ 正序允许重复取同类
多重背包 二进制分组(转 0/1) 将 $c_i$ 拆为 $1,2,4,\dots$ 与余数,做 0/1 $O(MV)$ $O(V)$ $M=\sum \lceil \log_2(c_i+1)\rceil$,实现简单、常数小
多重背包 单调队列优化 按模 $w_i$ 分组,对每组做长度 $c_i+1$ 的滑窗最大值 $O(nV)$ $O(V)$ 线性于 $V$,常数略大;适合 $c_i$ 大、$w_i$ 小/一般情形
  • 注:朴素完全背包的精确上界常写为 $\sum_i O!\left(\sum_{j=0}^{V}\big\lfloor \tfrac{j}{w_i}\big\rfloor\right)$=$\sum_i O!\left(\tfrac{V^2}{w_i}\right)$,在最坏 $w_i=1$ 时即 $O(nV^2)$