从一位小白处的取经感悟🐂🐂
0/1背包
注意01背包的满包问题
满包:其他状态为-inf【非法】
不满包:其他状态也为0【合法】
普通版
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
{
dp[i][j] = dp[i][j - 1]; //放不下的情况【拷贝操作】
if (j >= w[i])
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
printf("%d\n", dp[n][m]);
滚动数组优化版
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
{
dp[i & 1][j] = dp[i & 1][j - 1]; //放不下的情况【拷贝操作】
if (j >= w[i])//i状态只与i-1有关
dp[i & 1][j] = max(dp[(i - 1) & 1][j], dp[(i - 1) & 1][j - w[i]] + v[i]);
}
printf("%d\n", dp[n][m]);
一维优化版
dp[0] = 0;
for (int i = 1; i <= n; i++)//一维优化的原理是省去了每次的拷贝操作
for (int j = m; j >= w[i]; j--)//注意是倒序,顺序的话一个i阶段可能一个物品会被重复利用多次,不符合dp原则
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);//dp[j]利用的只是j之前的dp[j-w[i]]
printf("%d\n", dp[m]);
路径打印问题
eg:牛客一个小栗子Jury Compromise
#include <bits/stdc++.h>
#pragma GCC optimize(3, "Ofast", "inline")
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<long long, long long> pll;
const int N = 805;
const int mod = 2147483648;
#define IOS \
ios::sync_with_stdio(0); \
cin.tie(0); \
cout.tie(0);
ll n, m, tot, suma, sumb;
ll dp[25][N], d[205][25][N], a[205], b[205];
vector<ll> v;
void getback(ll n, ll m, ll k)
{
if (m == 0)
return;
ll tmp = d[n][m][k];
getback(tmp - 1, m - 1, k - (a[tmp] - b[tmp])); //
v.push_back(tmp); //
suma += a[tmp];
sumb += b[tmp];
}
int main()
{
// IOS;
while (~scanf("%lld%lld", &n, &m) && n)
{
for (ll i = 1; i <= n; i++)
scanf("%lld%lld", &a[i], &b[i]);
memset(dp, 0xcf, sizeof(dp));//初始化0xcf
dp[0][400] = 0; //
for (int i = 1; i <= n; i++) //阶段
{
for (int j = 0; j <= m; j++)
for (int k = 0; k <= N; k++)
d[i][j][k] = d[i - 1][j][k];
for (int j = m; j; j--) //倒序,j!=0
for (int k = 0; k <= N; k++)
{
if (k - (a[i] - b[i]) < 0 || k - (a[j] - b[j]) > 800) //
continue;
if (dp[j][k] < dp[j - 1][k - (a[i] - b[i])] + (a[i] + b[i]))
{
dp[j][k] = dp[j - 1][k - (a[i] - b[i])] + (a[i] + b[i]);
d[i][j][k] = i;
}
}
}
ll j = 400, k = 0, f;//偏移了400,400是修正值为什么base=400?这是很显然的,m上限为20人,当20人的d均为0,p均为20时,会出现辨控差为-400。修正后回避下标负数问题,区间整体平移,从[-400,400]映射到[0,800]。
while (dp[m][j + k] < 0 && dp[m][j - k] < 0) //找到与偏移值绝对值之差最小的k,即找到辩方和与控方和之差绝对值最小的。
k++;
if (dp[m][j + k] > dp[m][j - k])
f = j + k;
else
f = j - k;
v.clear();
suma = 0, sumb = 0;
getback(n, m, f);
printf("Jury #%lld\n", ++tot);
printf("Best jury has value %lld for prosecution and value %lld for defence:\n", suma, sumb);
for (int i = 0; i < v.size(); i++)
printf(" %lld ", v[i]);
printf("\n\n");
}
return 0;
}
牛客一个小栗子数字组合
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 100;
int n, m;
int dp[N][N], a[N];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
dp[0][0] = 1; //初始化
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
{
dp[i][j] += dp[i - 1][j];
if (j >= a[i])
dp[i][j] += dp[i - 1][j - a[i]]; //max改成了+
}
printf("%d\n", dp[n][m]);
return 0;
}
一维优化
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 100;
int n, m;
int dp[N], a[N];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
dp[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = m; j >= a[i]; j--)
dp[j] += dp[j - a[i]];
printf("%d\n", dp[m]);
return 0;
}
完全背包
注意内层循环是顺序的就可以了,这也对应了完全背包可以多次使用的特点
牛客小栗子自然数拆分Lunatic版
#include <bits/stdc++.h>
#pragma GCC optimize(3, "Ofast", "inline")
using namespace std;
typedef long long ll;
typedef pair<long long, long long> pll;
typedef pair<int, int> pii;
const int N = 5000;
const int mod = 2147483648;
ll n;
ll dp[N];
int main()
{
scanf("%lld", &n);
dp[0] = 1;//注意这里0拆分的话有一种方案数
for (int i = 1; i <= n; i++) //n种物品
for (int j = i; j <= n; j++) //背包容量也为n,顺序,物品可以无限次使用,对应完全背包
dp[j] = (dp[j] + dp[j - i]) % mod;//取模模好了,+=有问题
printf("%lld\n", dp[n] > 0 ? dp[n] - 1 : mod);
return 0;
}
多重背包
直接差分法
dp[0] = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= c[i]; j++)
for (int k = m; k >= v[i]; k--) //一维优化要逆序
dp[k] = max(dp[k], dp[k - v[i]] + w[i]);
for (int i = 1; i <= n; i++)
ans = max(ans, dp[i]);
printf("%d\n", ans);
二进制拆分法
- 一个正整数n,可以被分解成1,2,4,…,2(k-1),n-2k+1的形式。其中,k是满足n-2^k+1>0的最大整数。举个例子,假如给了我们价值为2,数量是10的物品,首先可以把10拆开,二进制是可以表示任何数字的,10就可以拆拆成1,2,4和3,也就是1,2,3,4的货物,价值为2,4,6,8,且每种只有1件,这正是我们需要的,用01背包来处理每一件货物,这就比以前的将货物分成10件来处理少了6件,这也是降低复杂度的关键。注意对应的体积和价值也都要有所改变
- 等价于将c[i]个物品拆分成log(c[i])个
第一种风格的二进制优化
for (i = 1; i <= n; i++)
{
for (k = 1; (k << 1) <= c[i]; k <<= 1)
for (j = m; j >= k * a[i]; j--) //优化后,对应的价值、体积也会成倍变化
dp[j] |= dp[j - k * a[i]];
ll tmp = c[i] - k + 1; //剩余的余数
for (j = m; j >= tmp * a[i]; j--)
dp[j] |= dp[j - tmp * a[i]];
}
第二种风格的二进制优化
for (ll i = 1; i <= n; i++)
{
for (ll k = 1; k <= c[i]; k *= 2)
{
c[i] -= k;//注意c[i]要减去
for (ll j = m; j >= k * a[i]; j--)
dp[j] |= dp[j - k * a[i]];
}
if (c[i])
{
for (ll j = m; j >= c[i] * a[i]; j--)
dp[j] |= dp[j - c[i] * a[i]];
}
}
牛客一道小栗子Coins
这道题直接拆分法会TLE,用二进制拆分法可以做,但是这里我们用另一种方法,贪心策略的dp
#include <bits/stdc++.h>
#pragma GCC optimize(3, "Ofast", "inline")
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
typedef pair<long long, long long> pll;
const int N = 2 * 1e5 + 10;
const int mod = 2147483648;
#define IOS \
ios::sync_with_stdio(0); \
cin.tie(0); \
cout.tie(0);
ll c[N], a[N], use[N];
ll n, m, ans;
bool dp[N];
int main()
{
// IOS;
while (cin >> n >> m && n + m)
{
memset(dp, 0, sizeof(dp));
dp[0] = 1, ans = 0;
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
for (int i = 1; i <= n; i++)
scanf("%lld", &c[i]);
for (int i = 1; i <= n; i++)
{
memset(use, 0, sizeof(use));
for (int j = a[i]; j <= m; j++)
if (use[j - a[i]] < c[i] && dp[j - a[i]] && !dp[j])
dp[j] |= dp[j - a[i]], use[j] = use[j - a[i]] + 1;
// else
// use[j] = 0;//可以不要这句话,因为之前memset初始化use为0了
}
for (int i = 1; i <= m; i++)
ans += dp[i];
printf("%lld\n", ans);
}
return 0;
}
单调队列优化
分组背包
注意每组至多选一个
memset(dp, 0, sizeof(dp));
dp[0] = 0;
for (int i = 1; i <= n; i++)//i是阶段
for (int j = m; j >= 0; j--)//i和j共同构成了状态
for (int k = 1; k <= c[i]; k++)//k是决策
if (j >= v[i][k]) //
dp[j] = max(dp[j], dp[j - v[i][k]] + w[i][k]);
总结
日常QAQ菜菜菜