滚动数组: 让数组滚动起来,每次都使用固定的几个存储空间,来达到压缩,节省存储空间的作用。
一般用于递推和动态规划中
一维数组
比如:求斐波那契数列第100项
long long arr[3];
arr[0] = 1;
arr[0] = 1;
for (int i = 2; i < 100; ++i)
arr[i%3] = arr[(i-1)%3] + arr[(i-2)%3];
二维数组
int dp[1000][1000];
for (int i = 1; i < 1000; ++i)
for (int j = 0; j < 1000; ++j)
dp[i][j] = dp[i-1][j] + dp[i][j-1];
运用滚动数组
int dp[2][1000];
for (int i = 1; i < 1000; ++i)
for (int j = 0; j < 1000; ++j)
dp[i%2][j] = dp[(i-1)%2][j] + dp[i%2][j-1];
0-1 背包
有n个物品和一个容量为c的背包,每个物品有重量w[i]和v[i]两种属性,
选取若干个物品放入背包使背包中的物品总价值最大且背包中物品的总重量不超过背包的容量。
设DP状态f[i][j]为在前i个物品中选取,容量为j的背包所能达到的最大价值
状态转移: 假设当前已经处理好了前i-1个物品的所有状态,
那么对于第i个物品,当其不放入背包时,背包的剩余容量不变,背包中物品的总价值也不变,故这种情况的最大价值为f[i-1][j];
当其放入背包时,背包的剩余容量会减小w[i],背包中物品的总价值会增大 v[i],故这种情况的最大价值为 f[i-1][j-w[i]]+v[i].
由此可以得出状态转移方程:
f[i][j] = max(f[i-1][j], f[i-1][j-w[i]]+v[i])
由于对f[i]有影响的只有f[i-1],可以去掉第一维,直接用f[i]表示处理到当前物品时背包容量为i的最大价值
洛谷 2871
#include <iostream>
using namespace std;
//int dp[2][12881];
int dp[12881];
int w[3403];
int v[3403];
int main()
{
int i, j, n, c;
cin >> n >> c;
for (i = 1; i <= n; ++i)
cin >> w[i] >> v[i];
// 常规滚动数组实现
//for (i = 1; i <= n; ++i)
// for (j = 1; j <= c; ++j)
// if (j >= w[i])
// dp[i%2][j] = max(dp[(i-1)%2][j], dp[(i-1)%2][j-w[i]]+v[i]);
// else
// dp[i%2][j] = dp[(i-1)%2][j];
//cout << dp[n%2][c] << endl;
// 对dp[i][]产生影响的只有dp[i-1][], 所以数组可以去掉一维
for (i = 1; i <= n; ++i)
for (j = c; j >= w[i]; --j)
dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
cout << dp[c] << endl;
return 0;
}
完全背包
完全背包与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。
由于, f[i][j-w[i]]已由f[i][j-2*w[i]]更新过,对于第i件物品,重复选取,实现如下:
for (j = w[i]; j <= t; ++j) // j 按从小到大顺序枚举,可实现重复选取
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
洛谷 1616
#include <iostream>
using namespace std;
long long dp[10000001];
int w[10001];
int v[10001];
int main()
{
int i, j, n, t;
cin >> t >> n;
for (int i = 1; i <= n; ++i)
cin >> w[i] >> v[i];
for (i = 1; i <= n; ++i)
for (j = w[i]; j <= t; ++j) // j 按从小到大顺序枚举,可实现重复选取
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
cout << dp[t] << endl;
return 0;
}
多重背包问题
多重背包与 0-1 背包的区别仅在于一个物品可以选取m次。
单调队列求滑动窗口最值问题
/*https://www.luogu.com.cn/problem/P1886 */
#include <iostream>
#include <list>
#include <vector>
using namespace std;
int arr[100001];
int main()
{
int i, n, k;
cin >> n >> k;
for (i = 0; i < n; ++i)
cin >> arr[i];
list<int> ls1;
list<int> ls2;
vector<int> vec1;
vector<int> vec2;
for (i = 0; i < n; ++i)
{
// 单调递减队列求滑动窗口最大值
while (ls1.size() > 0 && arr[ls1.back()] < arr[i]) // 新加入元素不满足单调递减
ls1.pop_back();
ls1.push_back(i);
if (i-ls1.front()+1 > k) // 队列中元素数量超过窗口大小
{
ls1.pop_front();
}
if (i+1 >= k)
vec1.push_back(arr[ls1.front()]);
// 单调递增队列求滑动窗口最小值
while (ls2.size() > 0 && arr[ls2.back()] > arr[i])
ls2.pop_back();
ls2.push_back(i);
if (i-ls2.front()+1 > k)
{
ls2.pop_front();
}
if (i+1 >= k)
vec2.push_back(arr[ls2.front()]);
}
for (i = 0; i < vec2.size(); ++i)
cout << vec2[i] << " ";
cout << endl;
for (i = 0; i < vec1.size(); ++i)
cout << vec1[i] << " ";
cout << endl;
return 0;
}
/* https://www.luogu.com.cn/problem/P1776 */
/* 朴素思想
for (i = 0; i < n; ++i) // 枚举物品种类
{
cin >> v >> w >> m;
for (j = W; j >= w; --j) // 枚举背包容量
for (k = 1; k <= m; ++k) // 枚举该种物品的数量
dp[j] = max(dp[j], dp[j - k*w] + k*v);
}
为了获取dp[j], 需要使用dp[j-w], dp[j-2w], ... , dp[j-k*w]的状态。
当取k件该种物品,需要为这k件物品预留k*w的空间,剩下的j-k*w的空间就是之前存储的状态。由于第二层循环的j是递减的,当下一次容量为j-1时,整个遍历过程可理解为向左平移一格。
依此类推,当j递减到j-w时就和初始的状态重合了,
所以根据第i个物品的w,我们可以将整个dp数组划分成w个等价类,
分别以0,1,2,…,w-1为开始元素。
用单调递减队列对
每个等价类f[i][j]=max(f[i−1][j],f[i−1][j−w]+v,f[i−1][j−2w]+2v,....+f[i−1][j−m*w]+m*v)求最大值,
时间复杂度就可以降至O(n*m)。
可看做求大小为m的滑动窗口最大值问题
*/
#include <iostream>
#include <vector>
#include <list>
using namespace std;
int main()
{
int n, W , i, j, k;
int v, w, m; // value, weight, amount
cin >> n >> W;
vector<int> dp(W+1, 0);
vector<int> prev; // 滚动数组,记录上一组dp[]的值
list<int> ls; // 单调递减队列求滑动窗口最大值,滑动窗口的大小为m[i]
for (i = 0; i < n; ++i)
{
prev = dp;
cin >> v >> w >> m;
for (j = 0; j < w; ++j)
{
ls.clear();
for (k = j; k <= W; k += w)
{
/* 对于当前种类的物品,他的数量最多有m个,重量为w,
要用到的最早的状态是全取时k-s*w,所以要保证队首元素要在这个范围内。*/
if (!ls.empty() && ls.front() < k - w*m)
ls.pop_front();
/* k - ls[head] 背包剩余容量
(k - ls[head])/w 背包剩余容量能存几个重量为w的物品
(k - ls[head])/w*v 取这几个物品的价值 */
if (!ls.empty())
dp[k] = max(dp[k], prev[ls.front()] + (k-ls.front())/w*v);
/* 为了保持容量队列单调递减,背包容量k的dp值应小于队尾元素的dp值 */
while (!ls.empty() && prev[ls.back()] + (k-ls.back())/w*v <= prev[k])
ls.pop_back();
ls.push_back(k);
}
}
}
cout << dp[W] << endl;
return 0;
}
/*
任何一个非负整数,可以用一个以1为首项,2为公比的等比数列中的数来表示
通过二进制优化,可将多重背包转换为0-1背包
*/
#include <iostream>
using namespace std;
const int MAXN = 10000;
int arrV[MAXN]; // value
int arrW[MAXN]; // weight
int dp[100001];
int main()
{
int n, W , i, j, k, cnt = 0;
int v, w, m; // value, weight, amount
cin >> n >> W;
for (i = 0; i < n; ++i)
{
cin >> v >> w >> m;
k = 1;
do {
m -= k;
arrV[cnt] = v*k;
arrW[cnt] = w*k;
++cnt;
k = k << 1;
} while (k <= m);
if (m > 0)
{
arrV[cnt] = v*m;
arrW[cnt] = w*m;
++cnt;
}
}
for (i = 0; i < cnt; ++i)
for (j = W; j >= arrW[i]; --j)
dp[j] = max(dp[j], dp[j - arrW[i]] + arrV[i]);
cout << dp[W] << endl;
return 0;
}