背包 DP
背包 DP
有 \(n\) 个物品,每个物品有一个体积 \(c_i\) 与价值 \(w_i\) 。
背包容量为 \(m\) ,选择若干物品装入背包,最大化价值和。
01 背包
特点:每个物品仅有一件。
设 \(f_{i, j}\) 表示考虑了前 \(i\) 个物品,所占容量为 \(j\) 的最大价值,则:
时间复杂度 \(O(nV)\) ,空间可以滚动到 \(O(V)\) (需要倒序循环,避免物品重复选取)。
for (int i = 1; i <= n; ++i)
for (int j = m; j >= c[i]; --j)
f[j] = max(f[j], f[j - c[i]] + w[i]);
如果只要判定存在性,还可以用 bitset
的位移、按位或操作将复杂度除去一个 \(\omega\) 。
AT_dp_e Knapsack 2
01 背包。
\(n \le 100\) ,\(V \le 10^9\) ,\(w_i \le 10^3\)
虽然背包容量很大,但是总价值上界很小。考虑对每个总价值求最小体积即可,时间复杂度 \(O(n \sum w)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 7;
int c[N], w[N], f[N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
int sum = 0;
for (int i = 1; i <= n; ++i)
scanf("%d%d", c + i, w + i), sum += w[i];
memset(f, inf, sizeof(f)), f[0] = 0;
for (int i = 1; i <= n; ++i)
for (int j = sum; j >= w[i]; --j)
f[j] = min(f[j], f[j - w[i]] + c[i]);
for (int i = sum; ~i; --i)
if (f[i] <= m)
return printf("%d\n", i), 0;
return 0;
}
HDU2639 Bone Collector II
求 01 背包的严格第 \(k\) 优解。
\(n \le 100\) ,\(m \le 1000\) ,\(k \le 30\)
增加一维用于记录当前状态下的前 \(k\) 优解即可,具体的设 \(f_{i, j, k}\) 表示记录了前 \(i\) 个物品中,选择的物品总体积为 \(j\) 时,能够得到的第 \(k\) 大的价值和,转移直接归并即可。
时间复杂度 \(O(nmk)\) ,空间复杂度 \(O(mk)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 7, M = 1e3 + 7, K = 3e1 + 7;
int w[N], c[N], f[M][K];
int T, n, m, k;
signed main() {
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= n; ++i)
scanf("%d", w + i);
for (int i = 1; i <= n; ++i)
scanf("%d", c + i);
for (int i = 0; i <= m; ++i)
memset(f[i] + 1, 0, sizeof(int) * k);
for (int i = 1; i <= n; ++i)
for (int j = m; j >= c[i]; --j) {
static int tmp1[K], tmp2[K];
for (int p = 1; p <= k; ++p)
tmp1[p] = f[j][p], tmp2[p] = f[j - c[i]][p] + w[i];
tmp1[k + 1] = tmp2[k + 1] = -1;
int pos = 1, cur1 = 1, cur2 = 1;
while (pos <= k && (cur1 <= k || cur2 <= k)) {
if (tmp1[cur1] > tmp2[cur2])
f[j][pos] = tmp1[cur1++];
else
f[j][pos] = tmp2[cur2++];
if (f[j][pos] != f[j][pos - 1])
++pos;
}
}
printf("%d\n", f[m][k]);
}
return 0;
}
[ABC221G] Jumping sequence
给定序列 \(d_{1 \sim n}\) ,初始在 \((0, 0)\) ,第 \(i\) 次可以选择向上下左右其中一个方向移动 \(d_i\) 的距离,求最后位于 \((A, B)\) 的方案数,若有解还需给出一组方案。
\(n \le 2000\) ,\(d_i \le 1800\)
考虑直接将二维平面转 \(45\) 度,则终点为 \((A - B, A + B)\) ,所有的走法都可以表示成 \((\pm d', \pm d')\) ,这样两维就是独立的了。
因为直接 \(\pm 1\) ,坐标范围就是 \(x \in [- |A|, |A|], y \in [- |B|, |B|]\) 的。考虑同时 \(+1\) 后 \(\div 2\) ,则转化为 \(0, 1\) ,坐标范围就是 \(x \in [0, |A|], y \in [0, |B|]\) 的。
然后就直接背包即可做到 \(O(\frac{n \sum d}{\omega})\) 。
#include <bits/stdc++.h>
using namespace std;
const char op[] = {'L', 'D', 'U', 'R'};
const int N = 2e3 + 7, V = 3.6e6 + 7;
bitset<V> f[N];
int d[N], ans[N];
int n, A, B;
signed main() {
scanf("%d%d%d", &n, &A, &B);
int sumd = 0;
for (int i = 1; i <= n; ++i)
scanf("%d", d + i), sumd += d[i];
if (abs(A) + abs(B) > sumd)
return puts("No"), 0;
int nA = A - B + sumd, nB = A + B + sumd;
if ((nA & 1) || (nB & 1))
return puts("No"), 0;
nA /= 2, nB /= 2;
if (nA > sumd || nB > sumd)
return puts("No"), 0;
f[0].set(0);
for (int i = 1; i <= n; ++i)
f[i] = f[i - 1] | (f[i - 1] << d[i]);
if (!f[n].test(nA) || !f[n].test(nB))
return puts("No"), 0;
for (int i = n; i; --i) {
if (!f[i - 1].test(nA))
nA -= d[i], ans[i] |= 1;
if (!f[i - 1].test(nB))
nB -= d[i], ans[i] |= 2;
}
puts("Yes");
for (int i = 1; i <= n; ++i)
putchar(op[ans[i]]);
return 0;
}
QOJ5979. Log Set
对于一个可重集 \(S \subseteq \mathbb{Z}\) ,它的所有子集和(包括空集)组成了一个大小为 \(2^{|S|}\) 的可重集 \(T\) 。
现在给出 \(T\) ,求满足条件的字典序最小的 \(|S|\) 。
保证 \(T\) 中不同元素的数量 \(\le 10^4\) ,保证有解且 \(|S| \le 60\)
先考虑 \(\min S \ge 0\) 的情况,不难发现此时一定有 \(\min T \in S\) ,不断做退背包即可。
接着考虑一般情况,可以发现最大值与次大值的差值一定是 \(S\) 中某个元素的绝对值,证明显然。
但是还需要判断其符号,有结论:01 背包改变一个物品 \(x\) 的正负,等价于将 DP 数组平移 \(x\) 个单位,证明也不困难。
那么可以强行钦定所有元素都是正数,算出偏移量 \(k\) ,问题转化为选出一个子集和为 \(k\) 。将所有元素排序后做 01 背包,然后贪心选取即可。
时间复杂度 \(O(n |S| \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e4 + 7;
set<ll> f[N];
ll a[N], b[N], ans[N];
int n, m;
inline ll solve() {
map<ll, ll> f;
for (int i = 1; i <= n; ++i)
f[a[i]] = b[i];
m = 0;
while (f.size() > 1) {
ans[++m] = prev(f.end())->first - prev(prev(f.end()))->first;
for (auto &it : f)
it.second -= f[it.first - ans[m]];
map<ll, ll> g;
for (auto it : f)
if (it.second)
g.insert(it);
f = g;
}
while (f.begin()->second > 1)
ans[++m] = 0, f.begin()->second /= 2;
return f.begin()->first;
}
signed main() {
int T;
scanf("%d", &T);
for (int task = 1; task <= T; ++task) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%lld", a + i);
for (int i = 1; i <= n; ++i)
scanf("%lld", b + i);
ll delta = -solve();
sort(ans + 1, ans + m + 1);
f[0] = {0};
for (int i = 1; i <= m; ++i) {
f[i] = f[i - 1];
for (ll it : f[i - 1])
f[i].emplace(it + ans[i]);
}
for (int i = m; i && delta; --i)
if (f[i - 1].find(delta - ans[i]) != f[i - 1].end())
delta -= ans[i], ans[i] = -ans[i];
sort(ans + 1, ans + m + 1);
printf("Case #%d: ", task);
for (int i = 1; i <= m; ++i)
printf("%lld ", ans[i]);
puts("");
}
return 0;
}
完全背包
特点:每个物品有数量无限。
将 01 背包改为正序循环即可,这样物品就可以重复选取,时空复杂度同 01 背包。
for (int i = 1; i <= n; ++i)
for (int j = c[i]; j <= m; ++j)
f[j] = max(f[j], f[j - c[i]] + w[i]);
P5391 [Cnoi2019] 青染之心
有一个大小为 \(m\) 的背包,\(n\) 次操作,操作有两种:
add x y
:表示加入一种体积为 \(x\) ,价值为 \(y\) 的物品。erase
:撤销上一次加入操作。每个操作结束后求完全背包最大价值。
\(n, m \le 2 \times 10^4\) ,ML = 64 MB
一个简单的暴力是直接对每个前缀保留完全背包,空间复杂度 \(O(nm)\) ,无法接受。
考虑离线建立操作树,每个点的结果可以从它的父亲推过来。操作本质就是按照 dfs 序给出了一棵树,然后把每一个节点到根的路径上的物品拎出来求完全背包。
考虑重链剖分,每一条重链只开一个DP数组。每次 dfs 时,先递归轻儿子,对其开一个新数组继承当前点的信息,然后直接用当前点的数组处理重儿子的答案。
时间复杂度 \(O(nm)\) ,空间复杂度 \(O(m \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e4 + 7, LOGN = 15;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int f[LOGN][N], fa[N], siz[N], son[N], sta[N], id[N], c[N], w[N], ans[N];
int n, m, top, tot, idx;
void dfs1(int u) {
siz[u] = 1, son[u] = -1;
for (int v : G.e[u]) {
dfs1(v), siz[u] += siz[v];
if (son[u] == -1 || siz[v] > siz[son[u]])
son[u] = v;
}
}
void dfs2(int u) {
for (int i = c[u]; i <= m; ++i)
f[idx][i] = max(f[idx][i], f[idx][i - c[u]] + w[u]);
ans[u] = f[idx++][m];
for (int v : G.e[u])
if (v != son[u])
memcpy(f[idx], f[idx - 1], sizeof(int) * (m + 1)), dfs2(v);
--idx;
if (~son[u])
dfs2(son[u]);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
char str[5];
scanf("%s", str);
if (str[0] == 'a') {
++tot;
scanf("%d%d", c + tot, w + tot);
G.insert(sta[top], tot), fa[tot] = sta[top];
sta[++top] = tot;
} else
--top;
id[i] = sta[top];
}
for (int i = 1; i <= tot; ++i)
if (!fa[i]) {
memset(f[0], 0, sizeof(int) * (m + 1));
dfs1(i), dfs2(i);
}
for (int i = 1; i <= n; ++i)
printf("%d\n", ans[id[i]]);
return 0;
}
多重背包
特点:第 \(i\) 个物品有 \(k_i\) 件。
二进制分组
如果直接暴力转化为 01 背包,时间复杂度是 \(O(V \sum k_i)\) 的,考虑用二进制分组优化。
二进制分组:记 \(n = 2^0 + 2^1 + \cdots + 2^k + (n - 2^{k + 1} + 1)\) ,则 \(0 \sim n\) 均可以用这 \(k + 2\) 个数表示。
时间复杂度 \(O(V \sum \log k_i)\) ,空间复杂度 \(O(V)\) 。
QOJ6355. 5
给定 \(a_{1 \sim n}\) ,记 \(S = \sum_{i = 1}^n a_i\) ,则保证至少有 \(\frac{S}{5}\) 个 \(1\) 。
定义二元组 \((k, T)\) 合法,当且仅当存在长度为 \(k\) 的子序列元素和为 \(T\) 。
求本质不同的合法二元组数量。
\(n, S \leq 2 \times 10^5\)
设 \(f_{i, j, k}\) 表示前 \(i\) 个数选了 \(j\) 个,和为 \(k\) 的可行性,转移枚举当前数的决策即可,时间复杂度 \(O(n^2 S)\) 。
考虑优化,题目给出的性质是“至少有 \(\frac{S}{5}\) 个数为 \(1\) “,考虑将这些 \(1\) 都消成 \(0\) ,即将所有数减去 \(1\) ,这样序列中就至少有 \(\frac{S}{5}\) 个数为 \(0\) 。
考察此时 DP 数组的形态,固定 \(i, k\) ,则可行的 \(j\) 只有 \(O(1)\) 个连续段,时间复杂度 \(O(n^2)\) 。
继续优化,发现之后 \(O(\sqrt{S})\) 个不同的数,对每种数字二进制分组,可以证明最后只会留下 \(O(\sqrt{S})\) 个数。
对这些数做多重背包即可,时间复杂度 \(O(n \sqrt{S})\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;
vector<pair<int, int> > f[N << 1], g[N << 1];
int a[N], cnt[N];
int n, s;
inline void insert(int val, int num) {
for (int i = 0; i <= n + s; ++i)
for (auto it : f[i])
g[i + val].emplace_back(it.first + num, it.second + num);
for (int i = 0; i <= n + s; ++i) {
vector<pair<int, int> > vec(f[i].size() + g[i].size());
merge(f[i].begin(), f[i].end(), g[i].begin(), g[i].end(), vec.begin());
f[i].clear(), g[i].clear();
for (auto it : vec) {
if (f[i].empty() || it.first > f[i].back().second + 1)
f[i].emplace_back(it);
else
f[i].back().second = max(f[i].back().second, it.second);
}
}
}
signed main() {
scanf("%d%d", &n, &s);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), ++cnt[a[i]];
f[n].emplace_back(0, cnt[1]);
for (int i = 0; i <= s; ++i) {
if (i == 1 || !cnt[i])
continue;
int k = cnt[i];
for (int j = 0; (1 << j) <= k; k -= 1 << j++)
insert((i - 1) << j, 1 << j);
if (k)
insert((i - 1) * k, k);
}
ll ans = 0;
for (int i = 0; i <= n + s; ++i)
for (auto it : f[i])
ans += it.second - it.first + 1;
printf("%lld", ans);
return 0;
}
单调队列优化
先考虑暴力,设 \(f_{i, j}\) 表示考虑了前 \(i\) 个物品、所占容量为 \(j\) 的最大价值,则:
考虑优化,注意到转移不改变 \(j \bmod w_i\) ,因此考虑设 \(g_{x, y} = f_{i, x \times w_i + y}, g'_{x, y} = f_{i - 1, x \times w_i + y}\) 其中 \(y \in [0, w_i)\) ,则:
不难用单调队列优化到 \(O(nm)\) 。
[ARC104D] Multiset Mean
有 \(nk\) 个物品,对于 \(i = 1, 2, \cdots, n\) ,权值为 \(i\) 的物品有 \(k\) 个。需要从中选出若干个数组成一个非空可重集,对于 \(x = 1, 2, \cdots, n\) ,求选出的可重集平均数为 \(x\) 的方案数 \(\bmod m\) 。
\(n, k \le 100\)
考虑将所有数减去 \(x\) ,这样 \(= 0\) 的部分可以随便选,正负只要选的数总和的绝对值相等即可。
设 \(f_{i, j}\) 表示 \(1 \sim i\) 能凑出和为 \(j\) 的可重集的方案数,答案即为:
\(f_{i, j}\) 的求解直接前缀和优化多重背包即可,时间复杂度 \(O(n^3 k)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 7;
int f[N][N * N * N], s[N * N * N];
int n, m, lim, Mod;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d%d%d", &n, &m, &Mod), lim = n * (n + 1) / 2 * m;
f[0][0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 0; j < i; ++j)
for (int k = 0; k * i + j <= lim; ++k) {
s[k] = add(k ? s[k - 1] : 0, f[i - 1][k * i + j]);
f[i][k * i + j] = dec(s[k], k - m - 1 >= 0 ? s[k - m - 1] : 0);
}
for (int i = 1; i <= n; ++i) {
int res = 0;
for (int j = 0; j <= lim; ++j)
res = add(res, 1ll * f[i - 1][j] * f[n - i][j] % Mod);
printf("%d\n", dec(1ll * (m + 1) * res % Mod, 1));
}
return 0;
}
判定存在性
设 \(f_{i, j}\) 表示考虑了前 \(i\) 个物品、总重量为 \(j\) 时第 \(i\) 个物品最多能剩下的数量,若 \(j\) 不能被表示则为 \(-1\) 。
转移时若 \(f_{i - 1, j} = 0\) ,则第 \(i\) 个物品不需要用,令 \(f_{i, j} \gets k_i\) ;否则令 \(f_{i, j} \gets f_{i - 1, j - a_i} - 1\) 。
时间复杂度 \(O(nV)\) 。
CF1481F AB Tree
给定一棵树,最开始所有点权均为 \(0\) ,需要给 \(x\) 个点附上 \(1\) 的权值,最小化所有 \(1 \to i\) 路径形成的 01 串中本质不同字符串的数量,并给出构造。
\(n \le 10^5\)
首先发现答案的一个下界是最大深度,可以在每一层都赋一样的点权时取到。事实上答案的上界是最大深度 \(+1\) ,下面给出构造。
逐层考虑,若某一层不能填入相同的数,设剩余出现次数较大的数为 \(c\) 。考虑将非叶子节点都填入 \(c\) ,这样不同处只会在叶子出现。设 \(m\) 为未确定的点的数量,因为非叶节点数量 \(\le \frac{m}{2} \le c\) ,因此一定能填满。
接下来问题转化为判定能否每一层都赋一样的点权。这是一个背包问题,物品就是每层的节点数量。
由于不同的深度只有根号种,时空复杂度 \(O(n \sqrt{n})\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, B = 5e2 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<int> vec[N];
int f[B][N], fa[N], dep[N], num[N], d[N], cnt[N];
char ans[N];
int n, x;
void dfs(int u) {
vec[dep[u] = dep[fa[u]] + 1].emplace_back(u);
for (int v : G.e[u])
dfs(v);
}
signed main() {
scanf("%d%d", &n, &x);
for (int i = 2; i <= n; ++i)
scanf("%d", fa + i), G.insert(fa[i], i);
dfs(1);
int mxd = *max_element(dep + 1, dep + n + 1);
for (int i = 1; i <= mxd; ++i)
++num[vec[i].size()];
memset(f[0], -1, sizeof(f[0])), f[0][0] = 0;
int m = 0;
for (int i = 1; i <= n; ++i) {
if (!num[i])
continue;
d[++m] = i;
for (int j = 0; j <= n; ++j)
f[m][j] = max(f[m - 1][j] >= 0 ? num[i] : -1, j >= i ? f[m][j - i] - 1 : -1);
}
ans[n + 1] = '\0';
if (~f[m][x]) {
printf("%d\n", mxd);
fill(ans + 1, ans + n + 1, 'b');
for (int i = m; i; x -= cnt[d[i]] * d[i], --i)
cnt[d[i]] = num[d[i]] - f[i][x];
for (int i = 1; i <= mxd; ++i) {
if (!cnt[vec[i].size()])
continue;
--cnt[vec[i].size()];
for (int it : vec[i])
ans[it] = 'a';
}
} else {
printf("%d\n", mxd + 1);
int y = n - x;
for (int i = 1; i <= mxd; ++i) {
if (vec[i].size() <= x) {
for (int it : vec[i])
ans[it] = 'a', --x;
} else if (vec[i].size() <= y) {
for (int it : vec[i])
ans[it] = 'b', --y;
} else if (x > y) {
for (int it : vec[i])
if (!G.e[it].empty())
ans[it] = 'a', --x;
for (int it : vec[i]) {
if (!ans[it]) {
if (x)
ans[it] = 'a', --x;
else
ans[it] = 'b', --y;
}
}
} else {
for (int it : vec[i])
if (!G.e[it].empty())
ans[it] = 'b', --y;
for (int it : vec[i]) {
if (!ans[it]) {
if (y)
ans[it] = 'b', --y;
else
ans[it] = 'a', --x;
}
}
}
}
}
puts(ans + 1);
return 0;
}
二维费用背包
P1855 榨取kkksc03
有 \(n\) 个任务需要完成,完成第 \(i\) 个任务需要产生 \(m_i\) 元的开支、花费 \(t_i\) 分钟。
现在有 \(T\) 分钟时间与 \(m\) 元钱来处理这些任务,求最多能完成多少任务。
\(n \le 100\) ,\(m, T \le 200\)
状态再加一维即可,时间复杂度 \(O(nmT)\) ,空间复杂度 \(O(mT)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e2 + 7;
int f[N][N];
int n, m, t;
signed main() {
scanf("%d%d%d", &n, &m, &t);
for (int i = 1; i <= n; ++i) {
int a, b;
scanf("%d%d", &a, &b);
for (int j = m; j >= a; --j)
for (int k = t; k >= b; --k)
f[j][k] = max(f[j][k], f[j - a][k - b] + 1);
}
int ans = 0;
for (int i = 0; i <= m; ++i)
for (int j = 0; j <= t; ++j)
ans = max(ans, f[i][j]);
printf("%d", ans);
return 0;
}
分组背包
P1757 通天之分组背包
有 \(n\) 件物品和一个大小为 \(m\) 的背包,第 \(i\) 个物品的价值为 \(w_i\) ,体积为 \(v_i\) 。
同时每个物品属于各自的组,同组内最多只能选择一个物品。
求背包能装载物品的最大总价值。
\(n, m \le 1000\)
其实是从在所有物品中选择一件变成了从当前组中选择一件,于是就对每一组进行一次 01 背包即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
vector<int> vec[N];
int a[N], b[N], f[N];
int n, m, tot;
signed main() {
scanf("%d%d", &m, &n);
for (int i = 1; i <= n; ++i) {
int bel;
scanf("%d%d%d", a + i, b + i, &bel);
tot = max(tot, bel), vec[bel].emplace_back(i);
}
for (int i = 1; i <= tot; ++i)
for (int j = m; ~j; --j)
for (int it : vec[i])
if (j >= a[it])
f[j] = max(f[j], f[j - a[it]] + b[it]);
printf("%d", f[m]);
return 0;
}
有依赖的背包
P1064 [NOIP2006 提高组] 金明的预算方案
金明有 \(n\) 元钱,想要买 \(m\) 个物品,第 \(i\) 件物品的价格为 \(v_i\) ,重要度为 \(p_i\) 。
有些物品是从属于某个主件物品的附件,即要买这个物品必须购买它的主件。
求所购买的物品的 \(\sum v_i \times p_i\) 的最大值。
\(n \le 3.2 \times 10^4\) ,\(m \le 60\)
先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
考虑分类讨论。对于一个主件和它的若干附件,有以下几种可能:只买主件,买主件 + 某些附件。因为这几种可能性只能选一种,所以可以将这看成分组背包。
#include <bits/stdc++.h>
using namespace std;
const int N = 3.2e4 + 7, M = 6e1 + 7;
vector<int> id[N];
int v[M], w[M], f[N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1, bel; i <= m; ++i) {
scanf("%d%d%d", v + i, w + i, &bel);
w[i] *= v[i], id[bel].emplace_back(i);
}
for (int it : id[0])
for (int k = n; k >= v[it]; --k) {
int x = id[it].size() >= 1 ? id[it][0] : -1, y = id[it].size() >= 2 ? id[it][1] : -1;
if (k >= v[it])
f[k] = max(f[k], f[k - v[it]] + w[it]);
if (~x && k >= v[it] + v[x])
f[k] = max(f[k], f[k - v[it] - v[x]] + w[it] + w[x]);
if (~y && k >= v[it] + v[y])
f[k] = max(f[k], f[k - v[it] - v[y]] + w[it] + w[y]);
if (~y && k >= v[it] + v[x] + v[y])
f[k] = max(f[k], f[k - v[it] - v[x] - v[y]] + w[it] + w[x] + w[y]);
}
printf("%d", f[n]);
return 0;
}
退背包
P4141 消失之物
给定 \(a_{1 \sim n}\) ,记 \(cnt(i, x)\) 表示去掉 \(a_i\) 后做 01 背包和为 \(x\) 的方案数。对于 \(i \in [1, n], x \in [1, m]\) ,求所有 \(cnt(i, x) \bmod 10\) 。
\(n, m \le 2000\)
先求出不删掉数的 01 背包,然后考虑删掉一个数。由于是计数 DP,只要少一次 \(a_i\) 的转移即可做到 \(O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7;
int a[N], f[N], g[N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
f[0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = m; j >= a[i]; --j)
f[j] = (f[j] + f[j - a[i]]) % 10;
for (int i = 1; i <= n; ++i) {
memcpy(g, f, sizeof(g));
for (int j = a[i]; j <= m; ++j)
g[j] = ((g[j] - g[j - a[i]]) % 10 + 10) % 10;
for (int j = 1; j <= m; ++j)
putchar('0' | g[j]);
puts("");
}
return 0;
}
另一种方法是分治,考虑求解 \(i \in [l, mid]\) 的答案,则 \([mid + 1, r]\) 里的 \(a_i\) 都可以选,另一边也是一样的,时间复杂度 \(O(nm \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7, LOGN = 12;
int a[N], f[LOGN][N];
int n, m;
void solve(int l, int r, int d) {
if (l == r) {
for (int i = 1; i <= m; ++i)
putchar(f[d][i] | '0');
puts("");
return;
}
int mid = (l + r) >> 1;
memcpy(f[d + 1], f[d], sizeof(int) * (m + 1));
for (int i = mid + 1; i <= r; ++i)
for (int j = m; j >= a[i]; --j)
f[d + 1][j] = (f[d + 1][j] + f[d + 1][j - a[i]]) % 10;
solve(l, mid, d + 1);
memcpy(f[d + 1], f[d], sizeof(int) * (m + 1));
for (int i = l; i <= mid; ++i)
for (int j = m; j >= a[i]; --j)
f[d + 1][j] = (f[d + 1][j] + f[d + 1][j - a[i]]) % 10;
solve(mid + 1, r, d + 1);
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
f[0][0] = 1, solve(1, n, 0);
return 0;
}
CF1111D Destroy the Colony
给定长度为 \(n\) 的字符串 \(s\) ,其中 \(n\) 为偶数。\(q\) 次询问,每次询问给出两位置 \(x, y\) ,求满足下列条件串 \(p\) 的方案数:
- \(p\) 可以通过 \(s\) 交换任意次任意位置得到。
- 同一个字符集中在同一半段。
- \(s_x, s_y\) 在同一半段。
\(n \le 10^5\),\(q \le 10^5\) ,\(|\sum| = 52\)
不难发现本质不同的询问只有 \(|\sum|^2\) 种。若已知每个字符在哪一段,则方案数为 \(\dfrac{(\frac{n}{2})!^2}{\prod cnt_i!}\) 。
设 \(g_{x, y}\) 表示钦定 \(x, y\) 在前半段的方案数,答案即为 \(2g_{x, y} \times \dfrac{(\frac{n}{2})!^2}{\prod cnt_i!}\) 。设 \(f\) 表示对 \(cnt\) 做 01 计数背包的结果,,记 \(f'\) 表示不考虑 \(x, y\) 时做 01 计数背包的结果,则 \(g_{x, y} = f'_{\frac{n}{2} - cnt_x - cnt_y}\) ,可以用退背包实现。
时间复杂度 \(O(n |\sum|^2 + n \log n)\) ,需要特殊处理 \(x = y\) 的情况,此时 \(g_{x, x} = f'_{\frac{n}{2} - cnt_x}\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e5 + 7, S = 52;
int a[N], fac[N], inv[N], invfac[N], cnt[S], ans[S][S];
char str[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void prework(int n) {
fac[0] = fac[1] = 1;
inv[0] = inv[1] = 1;
invfac[0] = invfac[1] = 1;
for (int i = 2; i <= n; ++i) {
fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[i] = 1ll * (Mod - Mod / i) * inv[Mod % i] % Mod;
invfac[i] = 1ll * invfac[i - 1] * inv[i] % Mod;
}
}
signed main() {
scanf("%s", str + 1);
n = strlen(str + 1);
for (int i = 1; i <= n; ++i)
a[i] = (islower(str[i]) ? str[i] - 'a' : str[i] - 'A' + 26), ++cnt[a[i]];
prework(n);
vector<int> f(n + 1);
f[0] = 1;
for (int i = 0; i < S; ++i)
if (cnt[i])
for (int j = n; j >= cnt[i]; --j)
f[j] = add(f[j], f[j - cnt[i]]);
int mul = 1ll * fac[n / 2] * fac[n / 2] % Mod;
for (int i = 0; i < S; ++i)
mul = 1ll * mul * invfac[cnt[i]] % Mod;
auto solve = [&](int x, int y) -> int {
if (!cnt[x] || !cnt[y])
return 0;
if (x == y) {
if (cnt[x] > n / 2)
return 0;
vector<int> g = f;
for (int i = cnt[x]; i <= n; ++i)
g[i] = dec(g[i], g[i - cnt[x]]);
return 2ll * mul * g[n / 2 - cnt[x]] % Mod;
} else {
if (cnt[x] + cnt[y] > n / 2)
return 0;
vector<int> g = f;
for (int i = cnt[x]; i <= n; ++i)
g[i] = dec(g[i], g[i - cnt[x]]);
for (int i = cnt[y]; i <= n; ++i)
g[i] = dec(g[i], g[i - cnt[y]]);
return 2ll * mul * g[n / 2 - cnt[x] - cnt[y]] % Mod;
}
};
for (int i = 0; i < S; ++i)
for (int j = 0; j < S; ++j)
ans[i][j] = solve(i, j);
scanf("%d", &m);
while (m--) {
int x, y;
scanf("%d%d", &x, &y);
printf("%d\n", ans[a[x]][a[y]]);
}
return 0;
}
按位或
给定整数序列 \(a_{1 \sim m}\) ,其中 \(a_i \in [1, 2^n - 1]\) ,从 \(2^m\) 种选数方案中数字按位或的和的种类数量。
\(n \le 28\)
考虑 \(x\) 是否能被或出来,显然只要贪心选取 \(a_i \subseteq x\) 的所有 \(a_i\) 判定即可。令 \(f_{a_i} = 1\) ,则做一遍高维前缀或可以做到 \(O(2^n n)\) 。
考虑令 \(f_{a_i} = a_i\) ,称 \(x\) 在第 \(d\) 位上合法当且仅当 \(x\) 与高维前缀或后的 \(f_x\) 在第 \(d\) 位上相同,把这个 01 序列记作 \(g_d\) ,则 \(f = \operatorname{and}_{i = 0}^{n - 1} g_i\) ,具体的就是不合法的 \(x\) 某一位一定或不出来。算单个 \(g_i\) 可以压位优化到 \(O(\frac{2^n n}{\omega})\) ,时间复杂度 \(O(\frac{2^n n^2}{\omega})\) 。
发现这个 \(g_i\) 等价于 FMT 不枚举第 \(i\) 位,然后直接上退背包分治即可做到 \(O(\frac{2^n n \log n}{\omega})\) ,用 unsigned long long
时 \(\omega = 64\) 。
#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int N = 28, All = 63, B = 6;
ull f[B][1 << (N - B)], g[B], ans[1 << (N - B)];
int n, m, k;
inline void FMT(ull *f, int d) {
if (d < B) {
for (int i = 0; i < k; ++i)
f[i] |= (f[i] & g[d]) << (1 << d);
} else {
for (int i = 0; i < k; ++i)
if (i >> (d - B) & 1)
f[i] |= f[i ^ (1 << (d - B))];
}
}
void solve(int l, int r, int d) {
if (l == r) {
for (int i = 0; i < k; ++i)
ans[i] &= f[d][i];
return;
}
int mid = (l + r) >> 1;
memcpy(f[d + 1], f[d], sizeof(ull) * k);
for (int i = mid + 1; i <= r; ++i)
FMT(f[d + 1], i);
solve(l, mid, d + 1);
memcpy(f[d + 1], f[d], sizeof(ull) * k);
for (int i = l; i <= mid; ++i)
FMT(f[d + 1], i);
solve(mid + 1, r, d + 1);
}
signed main() {
for (int i = 0; i < B; ++i)
for (int j = 0; j <= All; ++j)
if (~j >> i & 1)
g[i] |= 1ull << j;
scanf("%d%d", &n, &m);
k = 1 << max(n - B, 0), f[0][0] = 1;
for (int i = 1; i <= m; ++i) {
int x;
scanf("%d", &x);
f[0][x >> B] |= 1ull << (x & All);
}
memset(ans, -1, sizeof(ans));
solve(0, n - 1, 0);
int answer = 0;
for (int i = 0; i < k; ++i)
answer += __builtin_popcountll(ans[i]);
printf("%d", answer);
return 0;
}
P6808 [BalticOI 2010] Candies (Day2)
给定正整数序列 \(a_{1 \sim n}\) ,称 \(m\) 能被表示当且仅当其为 \(a_{1 \sim n}\) 的一个子序列的和。
修改序列中的一个元素 \(P \to Q\) ,最大化能表示的数的数量,在该前提下最小化 \(P\) ,在最小化 \(P\) 的前提下最小化 \(Q\) ,其中 \(P, Q\) 均为正整数。
\(n \le 100\) ,\(a_i \le 7000\)
首先可以发现,若将某个数修改成极大值时,能表示的数的数量取到修改该数的最大值。
先考虑修改 \(a_i\) 后能表示的数的数量,设删去 \(a_i\) 后能表示的数的数量为 \(k\) ,则修改 \(a_i\) 的答案为 \(2k + 1\) ,\(k\) 不难通过退背包求解,因此可以确定 \(P\) 。
注意这里是可行性的退背包,考虑将可行性转计数,但是方案数太大,考虑模上大质数,而方案数恰为其倍数的可能性很小,因此正确性可以接受。或者也可以分治,但是会多一个 \(\log\) 。
接下来考虑求 \(Q\) ,设去掉一个 \(P\) 后剩下的数集为 \(A\) ,不难发现要取到上界,\(Q\) 必须满足 \(A \cap \{ x \mid x - Q \in A \} = \emptyset\) 。若该条件不合法,则一定存在 \(S_1, S_2 \in A\) 满足 \((\sum S_1) - (\sum S_2) = Q\) 。因此只要对其做背包,找到最小不能被表示的数即可,注意值域为 \([- \sum a, \sum a]\) 。
时间复杂度 \(O(n^2 A)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e2 + 7, M = 1.4e6 + 7;
int a[N], f[M];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), m += a[i];
f[0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = m; j >= a[i]; --j)
f[j] = add(f[j], f[j - a[i]]);
int cnt = 0, ans = m + 1;
for (int i = 1; i <= n; ++i) {
for (int j = a[i]; j <= m; ++j)
f[j] = dec(f[j], f[j - a[i]]);
int res = m - count(f + 1, f + m + 1, 0);
if (res > cnt)
cnt = res, ans = a[i];
else if (res == cnt && a[i] < ans)
ans = a[i];
for (int j = m; j >= a[i]; --j)
f[j] = add(f[j], f[j - a[i]]);
}
printf("%d ", ans);
memset(f, 0, sizeof(f)), f[m] = 1;
for (int i = 1; i <= n; ++i) {
if (a[i] == ans) {
ans = -1;
continue;
}
for (int j = m * 2; j - a[i] >= 0; --j)
f[j] |= f[j - a[i]];
for (int j = 0; j + a[i] <= m * 2; ++j)
f[j] |= f[j + a[i]];
}
ans = 1;
while (f[ans + m])
++ans;
printf("%d", ans);
return 0;
}
[AGC049D] Convex Sequence
给定 \(n, m\) ,求满足以下条件的长度为 \(n\) 的非负整数数列 \(a_{1 \sim n}\) 的数量 \(\bmod (10^9 + 7)\) :
- \(\sum_{i = 1}^n a_i = m\) 。
- \(\forall 2 \le i \le n - 1, 2a_i \le a_{i - 1} + a_{i + 1}\) 。
\(n, m \le 10^5\)
考虑转化第二个条件为 \(a_i - a_{i - 1} \le a_{i + 1} - a_i\) ,即整个序列是下凸的。考虑枚举第一个最小值的位置 \(i\) ,则可以用三类操作构造所有合法的序列:
- 全局加一。
- 选择 \(j < i\) ,对 \(a_j, a_{j - 1}, \cdots, a_1\) 分别加上 \(1, 2, \cdots, j\) 。
- 选择 \(j > i\) ,对 \(a_j, a_{j + 1}, \cdots, a_n\) 分别加上 \(1, 2, \cdots, n - j + 1\) 。
并且转化为操作序列的好处是,所有操作序列与最终的序列一一对应,因此可以对操作序列计数。
而操作序列的每个操作是不区分顺序的,并且每种操作对全局和的贡献是一定的,因此只需要做完全背包即可,具体的物品为 \(n, 1 \sim i - 1, 1 \sim n - i\) 。
考虑对所有第一个最小值的位置 \(i\) 计数,不难发现 \(i \to i + 1\) 时只会删去并加入各一个数,不难用退背包实现。注意由于钦定了 \(i\) 为第一个最小值,因此 \(j = i - 1\) 必须操作一次。
注意到合法的 \(j\) 为 \(O(\sqrt{m})\) 级别, 因此时间复杂度 \(O(m \sqrt{m})\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e5 + 7;
int f[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline void insert(int k) {
for (int i = k; i <= m; ++i)
f[i] = add(f[i], f[i - k]);
}
inline void remove(int k) {
for (int i = m; i >= k; --i)
f[i] = dec(f[i], f[i - k]);
}
signed main() {
scanf("%d%d", &n, &m);
f[0] = 1, insert(n);
for (int i = 1; i <= n && i * (i + 1) / 2 <= m; ++i)
insert(i * (i + 1) / 2);
int ans = 0;
for (int i = 1; i <= n; ++i) {
if (1ll * (n - i + 1) * (n - i + 2) / 2 <= m)
remove(1ll * (n - i + 1) * (n - i + 2) / 2);
if (i > 1 && 1ll * (i - 1) * i / 2 <= m)
insert(1ll * (i - 1) * i / 2);
if (1ll * (i - 1) * i / 2 <= m)
ans = add(ans, f[m - 1ll * (i - 1) * i / 2]);
}
printf("%d", ans);
return 0;
}
[ARC028D] 注文の多い高橋商店
给定 \(n\) 种物品,第 \(i\) 种物品有 \(a_i\) 个。并给出常数 \(m\) ,表示需要拿走 \(m\) 个物品。
\(q\) 次询问,每次给出 \(x, k\) ,求第 \(x\) 个物品恰好拿走 \(k\) 个时拿走 \(m\) 个物品的方案数 \(\bmod (10^9 + 7)\) 。
\(n, m \le 2000\) ,\(q \le 5 \times 10^5\)
问题相当于去掉第 \(x\) 种物品后拿走 \(m - k\) 个物品的方案数,对多重背包退背包即可,时间复杂度 \(O(nm + q)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2e3 + 7;
int a[N], f[N], ans[N][N];
int n, m, q;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
signed main() {
scanf("%d%d%d", &n, &m, &q);
f[0] = 1;
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i);
for (int j = 1; j <= m; ++j)
f[j] = add(f[j], f[j - 1]);
for (int j = m; j > a[i]; --j)
f[j] = dec(f[j], f[j - a[i] - 1]);
}
for (int i = 1; i <= n; ++i) {
for (int j = a[i] + 1; j <= m; ++j)
f[j] = add(f[j], f[j - a[i] - 1]);
for (int j = m; j; --j)
f[j] = dec(f[j], f[j - 1]);
memcpy(ans[i], f, sizeof(f));
for (int j = 1; j <= m; ++j)
f[j] = add(f[j], f[j - 1]);
for (int j = m; j > a[i]; --j)
f[j] = dec(f[j], f[j - a[i] - 1]);
}
while (q--) {
int x, k;
scanf("%d%d", &x, &k);
printf("%d\n", k > m ? 0 : ans[x][m - k]);
}
return 0;
}
树上背包
P2014 [CTSC1997] 选课
给出 \(n\) 个物品,所有物品的依赖关系形成森林(若存在父亲则必须先选父亲),求选 \(m\) 个物品的最大价值。
\(n, m \le 300\)
为了方便,考虑新增一个价值为 \(0\) 的物品(令其编号为 \(0\) )作为根,这样就将森林变成了树。
设 \(f_{u, i, j}\) 表示 \(u\) 子树内考虑了前 \(i\) 棵子树,选了 \(j\) 个物品的最大价值,则:
该做法的时间复杂度为 \(O(nm)\) ,空间可以滚动数组优化到 \(O(nm)\) 。
时间复杂度证明:考虑合并两个子树 \(x, y\) 对时间复杂度的贡献:
- \(siz_x, siz_y \le m\) :由于这类合并只会在每个极大的 \(siz \le m\) 的子树内部产生,并且极大的 \(siz \le m\) 的子树不交,显然均分这些极大的子树可以卡到复杂度上界,该部分复杂度为 \(O(\sum siz^2) = O(\frac{n}{m} \times m^2) = O(nm)\) 。
- \(siz_x \le m, siz_y > m\) :那么合并的复杂度为 \(O(siz_x \times m)\) ,由于所有这样的 \(x\) 的 \(siz\) 和 \(< n\) ,因此该部分复杂度为 \(O(nm)\) 。
- \(siz_x, siz_y > m\) :这样的合并显然不超过 \(O(\frac{n}{m})\) 次,因此该部分复杂度为 \(O(\frac{n}{m} \times m^2) = O(nm)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e2 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int f[N][N], fa[N], s[N], siz[N];
int n, m;
void dfs(int u) {
siz[u] = 1, f[u][1] = s[u];
for (int v : G.e[u]) {
dfs(v);
for (int i = min(siz[u], m + 1); i; --i)
for (int j = 1; j <= siz[v] && i + j <= m + 1; ++j)
f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]);
siz[u] += siz[v];
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%d%d", fa + i, s + i);
G.insert(fa[i], i);
}
dfs(0);
printf("%d", f[0][m + 1]);
return 0;
}
子树卷积
给定一棵树,第 \(i\) 个点有重量 \(b_i\) 、权值 \(a_i\) 的物品,定义一组选物品的方案的权值为所选物品的权值乘积。
对于所有 \(1 \le x \le n, 1 \le i \le m\) ,求 \(x\) 子树内选出物品重量和为 \(i\) 的所有方案的权值和。
首先引入三个做法:
-
算法一:直接 NTT,时间复杂度 \(O(nm \log m)\) 。
-
算法二:直接暴力,只对有值的位置进行转移,时间复杂度 \(O(\frac{nm^2}{\log m})\) ,分析与树上背包类似。
-
算法三:dsu-on-tree,暴力将轻子树的每个点插入,时间复杂度 \(O(nm \log n)\) 。
考虑综合三个做法,合并两个子树 \(x, y\) 时分类讨论:
- \(siz_x, siz_y \le \frac{1}{2} \log m\) :使用算法二,该部分时间复杂度为 \(O(\sum 2^{siz_x} \times 2^{siz_y}) = O(nm)\) 。
- \(siz_x \le \frac{1}{2} \log m, siz_y > \frac{1}{2} \log m\) :使用算法三,该部分时间复杂度为 \(O(nm)\) 。
- \(siz_x, siz_y > \frac{1}{2} \log m\) :使用算法一,该部分时间复杂度为 \(O(\frac{n}{\frac{1}{2} \log m} \times m \log m) = O(nm)\) 。
总时间复杂度 \(O(nm)\) 。
QOJ7895. Graph Partitioning 2
给定一棵树和常数 \(k\) ,求有多少种断边的方式使得树的每个连通块的大小都是 \(k\) 或 \(k + 1\) 。
\(n \le 10^5\)
考虑对 \(k\) 根号分治。
若 \(k \le \sqrt{n}\) ,记录当前包含子树根的还没切出去的块大小做树上背包,时间复杂度 \(O(n \sqrt{n})\) 。
若 \(k > \sqrt{n}\) ,记录子树内切出去 \(i\) 个大小为 \(k\) 的块,显然 \(i \le \frac{siz}{k}\) ,转移类似树上背包,时间复杂度 \(O(n \sqrt{n})\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e5 + 7, M = 319;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int f[N][M], g[M], siz[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
void dfs1(int u, int fa) {
memset(f[u], 0, sizeof(f[u]));
siz[u] = 1, f[u][1] = 1;
for (int v : G.e[u]) {
if (v == fa)
continue;
dfs1(v, u);
memset(g, 0, sizeof(g));
for (int i = 0; i <= min(siz[u], m + 1); ++i)
for (int j = 0; j <= min(siz[v], m); ++j)
if (i + j <= m + 1)
g[i + j] = add(g[i + j], 1ll * f[u][i] * f[v][j] % Mod);
memcpy(f[u], g, sizeof(g)), siz[u] += siz[v];
}
f[u][0] = add(f[u][0], add(f[u][m], f[u][m + 1]));
}
void dfs2(int u, int fa) {
memset(f[u], 0, sizeof(f[u]));
siz[u] = 1, f[u][0] = 1, f[u][1] = (m == 1);
for (int v : G.e[u]) {
if (v == fa)
continue;
dfs2(v, u), memset(g, 0, sizeof(g));
for (int i = 0; i * m <= siz[u]; ++i)
for (int j = 0; j * m <= siz[v]; ++j) {
int x = (siz[u] - i * m) % (m + 1), y = (siz[v] - j * m) % (m + 1);
if (x + y > m + 1 || (!x && y))
continue;
if (x + y == m && y)
g[i + j + 1] = add(g[i + j + 1], 1ll * f[u][i] * f[v][j] % Mod);
g[i + j] = add(g[i + j], 1ll * f[u][i] * f[v][j] % Mod);
}
memcpy(f[u], g, sizeof(g)), siz[u] += siz[v];
}
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
G.clear(n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
if (1ll * m * m <= n) {
dfs1(1, 0);
printf("%d\n", f[1][0]);
} else {
dfs2(1, 0);
int ans = 0;
for (int i = 0; i * m <= n; ++i)
if (!((n - i * m) % (m + 1)))
ans = add(ans, f[1][i]);
printf("%d\n", ans);
}
}
return 0;
}
发现每次暴力卷积比较傻,考虑继续优化。若合并过来的子树 \(siz < k\) ,那么这个合并过程可以均摊总复杂度线性,否则直接暴力 NTT 处理。由于 NTT 最多只会进行 \(O(\frac{n}{k})\) 次,因此时间复杂度 \(O(n \log k)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 998244353;
const int N = 1e5 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
deque<int> f[N];
int siz[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
namespace Poly {
const int rt = 3, invrt = (Mod + 1) / 3;
vector<int> rev;
inline int calc(int n) {
int len = 1;
while (len < n)
len <<= 1;
rev.resize(len);
for (int i = 0; i < len; ++i)
rev[i] = (rev[i >> 1] >> 1) | (i & 1 ? len >> 1 : 0);
return len;
}
inline void NTT(deque<int> &f, int op) {
int n = f.size();
for (int i = 0; i < n; ++i)
if (i < rev[i])
swap(f[i], f[rev[i]]);
for (int k = 1; k < n; k <<= 1) {
int tG = mi(op == 1 ? rt : invrt, (Mod - 1) / (k << 1));
for (int i = 0; i < n; i += k << 1) {
int buf = 1;
for (int j = 0; j < k; ++j) {
int fl = f[i + j], fr = 1ll * buf * f[i + j + k] % Mod;
f[i + j] = add(fl, fr), f[i + j + k] = dec(fl, fr);
buf = 1ll * buf * tG % Mod;
}
}
}
if (op == -1) {
int invn = mi(n, Mod - 2);
for (int i = 0; i < n; ++i)
f[i] = 1ll * f[i] * invn % Mod;
}
}
inline void Mul(deque<int> &f, deque<int> &g) {
int lim = f.size() + g.size() - 1, len = calc(lim);
f.resize(len), g.resize(len);
NTT(f, 1), NTT(g, 1);
for (int i = 0; i < len; ++i)
f[i] = 1ll * f[i] * g[i] % Mod;
NTT(f, -1), f.resize(lim);
}
} // namespace Poly
void dfs(int u, int fa) {
siz[u] = 1, f[u].clear();
int sum = 1;
for (int v : G.e[u])
if (v != fa) {
dfs(v, u), siz[u] += siz[v];
if (siz[v] < m)
sum += siz[v];
else if (f[u].empty())
swap(f[u], f[v]);
else {
Poly::Mul(f[u], f[v]), f[v].clear();
if (f[u].size() > m + 2)
f[u].resize(m + 2);
}
}
if (siz[u] < m)
return;
if (f[u].empty())
f[u] = {1};
while (sum--)
f[u].emplace_front(0);
if (f[u].size() > m) {
f[u][0] = add(f[u][0], f[u][m]);
if (f[u].size() > m + 1)
f[u][0] = add(f[u][0], f[u][m + 1]), f[u].resize(m + 1);
}
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
G.clear(n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs(1, 0);
printf("%d\n", f[1][0]);
}
return 0;
}
HDU6566 The Hanged Man
给出一棵树,每个点有 \(a_i, b_i\) 两个权值。对于 \(k \in [1, m]\) ,求满足 \(\sum a = k\) 的独立集中 \(\sum b\) 最大的方案数。
\(n \le 50\) ,\(m \le 5000\)
设 \(f_{u, 0/1, i}\) 表示考虑 \(u\) 子树、\(u\) 是否选择、\(\sum a = i\) 时最大的 \(\sum b\) ,方案数直接转移的时候顺带统计即可。
考虑将合并 DP 数组的方式改为单点加入,dfs 整棵树同时 DP,重新设 \(f_{s, i}\) 表示祖先选择状态为 \(s\) 、\(\sum a = i\) 时最大的 \(\sum b\) ,直接 dfs 可以做到 \(O(n 2^n m)\) 。
考虑继续优化,注意到指数的部分为树高,考虑利用点分树的性质:
- 最大深度 \(\le 1 + \log n\) 。
- 原来一条边连接的两个点在点分树上一定是祖先关系。
因此考虑在点分树上 DP,只要记录点分树上祖先的选择状态即可做到 \(O(n^2 m)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e1 + 7, M = 5e3 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
ll f[N << 1][M], g[N << 1][M];
int a[N], b[N], fa[N], siz[N], mxsiz[N], sta[N];
bool vis[N];
int n, m, root;
void dfs(int u, int f) {
fa[u] = f;
for (int v : G.e[u])
if (v != f)
dfs(v, u);
}
int getsiz(int u, int f) {
siz[u] = 1;
for (int v : G.e[u])
if (!vis[v] && v != f)
siz[u] += getsiz(v, u);
return siz[u];
}
void getroot(int u, int f, int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (int v : G.e[u])
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
inline bool beside(int x, int y) {
return fa[x] == y || fa[y] == x;
}
void solve(int u, int d) {
vis[u] = true;
for (int s = 0; s < (1 << d); ++s) {
bool flag = true;
for (int i = 0; i < d; ++i)
if ((s >> i & 1) && beside(u, sta[i])) {
flag = false;
break;
}
if (flag) {
int t = s | 1 << d;
for (int i = a[u]; i <= m; ++i)
if (~f[s][i - a[u]])
f[t][i] = f[s][i - a[u]] + b[u], g[t][i] = g[s][i - a[u]];
}
}
sta[d] = u;
for (int v : G.e[u])
if (!vis[v])
root = 0, getroot(v, u, getsiz(v, u)), solve(root, d + 1);
for (int s = 0; s < (1 << d); ++s) {
int t = s | 1 << d;
for (int i = 0; i <= m; ++i) {
if (f[t][i] > f[s][i])
f[s][i] = f[t][i], g[s][i] = g[t][i];
else if (f[t][i] == f[s][i])
g[s][i] += g[t][i];
f[t][i] = -1, g[t][i] = 0;
}
}
}
signed main() {
int T;
scanf("%d", &T);
for (int task = 1; task <= T; ++task) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d%d", a + i, b + i);
G.clear(n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs(1, 0);
memset(f, -1, sizeof(f)), memset(g, 0, sizeof(g));
f[0][0] = 0, g[0][0] = 1;
memset(vis, false, sizeof(vis));
root = 0, getroot(1, 0, n), solve(root, 0);
printf("Case %d:\n", task);
for (int i = 1; i <= m; ++i)
printf("%lld ", g[0][i]);
puts("");
}
return 0;
}
QOJ7419. Jiry Matchings
给定一棵树,边带权。对于 \(k = 1, 2, \cdots, n - 1\) ,求匹配数为 \(k\) 的最大权匹配,或报告无解。
\(n \le 2 \times 10^5\)
设 \(f_{u, i, 0/1}\) 表示 \(u\) 子树内选了 \(i\) 个匹配,\(u\) 是否被选的答案,注意到 \(f\) 关于 \(i\) 是凸的,因此直接做闵可夫斯基和即可做到 \(O(n^2)\) 。
考虑用轻重链剖分优化。对于一个点的轻儿子,考虑带权分治合并信息,记 \(g_{l, r, 0/1}\) 表示是否保留与 \(u\) 匹配的点,该部分复杂度 \(O(n \log n)\) 。
时间复杂度证明:每个点都挂着一个一次式,每在分治树上条一层或在树上跳轻边,子树大小都翻倍,因此一个点只会被卷 \(O(\log n)\) 次。
对于一条重链直接分治,设 \(g_{l, r, 0/1, 0/1}\) 表示区间 \([l, r]\) 两端点是否选择的答案,该部分复杂度 \(O(n \log^2 n)\) 。
总时间复杂度 \(O(n \log^2 n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 2e5 + 7;
struct Graph {
vector<pair<int, int> > e[N];
inline void insert(int u, int v, int w) {
e[u].emplace_back(v, w);
}
} G;
vector<ll> f[N][2];
int fa[N], val[N], siz[N], son[N];
int n;
void dfs1(int u, int f) {
fa[u] = f, siz[u] = 1;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (v == f)
continue;
val[v] = w, dfs1(v, u), siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v;
}
}
inline vector<ll> Minkowski(vector<ll> a, vector<ll> b) {
if (a.empty() || b.empty())
return {};
vector<ll> c(a.size() + b.size() - 1);
c[0] = a[0] + b[0];
for (int i = a.size() - 1; i; --i)
a[i] -= a[i - 1];
for (int i = b.size() - 1; i; --i)
b[i] -= b[i - 1];
merge(a.begin() + 1, a.end(), b.begin() + 1, b.end(), c.begin() + 1, greater<ll>());
for (int i = 1; i < c.size(); ++i)
c[i] += c[i - 1];
return c;
}
inline vector<ll> merge(vector<ll> a, vector<ll> b) {
vector<ll> c(max(a.size(), b.size()));
for (int i = 0; i < c.size(); ++i)
c[i] = max(i < a.size() ? a[i] : -inf, i < b.size() ? b[i] : -inf);
return c;
}
array<vector<ll>, 2> solve1(int l, int r, vector<int> &id) {
if (l == r) {
auto g = f[id[l]][0];
if (!g.empty()) {
for (ll &it : g)
it += val[id[l]];
g.insert(g.begin(), -inf);
}
return {merge(f[id[l]][0], f[id[l]][1]), g};
}
int mid = (l + r) >> 1;
auto gl = solve1(l, mid, id), gr = solve1(mid + 1, r, id);
return {Minkowski(gl[0], gr[0]), merge(Minkowski(gl[1], gr[0]), Minkowski(gl[0], gr[1]))};
}
array<array<vector<ll>, 2>, 2> solve2(int l, int r, vector<int> &id) {
if (l == r)
return {(array<vector<ll>, 2>){f[id[l]][0], (vector<ll>){}},
(array<vector<ll>, 2>){(vector<ll>){}, f[id[l]][1]}};
int sum = 0;
for (int i = l; i <= r; ++i)
sum += siz[id[i]] - siz[son[id[i]]];
int mid = l, now = 0;
while (mid < r && now < sum / 2)
now += siz[id[mid]] - siz[son[id[mid]]], ++mid;
auto gl = solve2(l, mid - 1, id), gr = solve2(mid, r, id);
array<array<vector<ll>, 2>, 2> res;
for (int i = 0; i <= 1; ++i)
for (int j = 0; j <= 1; ++j)
for (int p = 0; p <= 1; ++p)
for (int q = 0; q <= 1; ++q)
res[i][q] = merge(res[i][q], Minkowski(gl[i][j], gr[p][q]));
for (int i = 0; i <= 1; ++i)
for (int j = 0; j <= 1; ++j) {
auto g = Minkowski(gl[i][0], gr[0][j]);
if (g.empty())
continue;
for (ll &it : g)
it += val[id[mid]];
g.insert(g.begin(), -inf);
res[i || (l == mid - 1)][j || (mid == r)] = merge(res[i || (l == mid - 1)][j || (mid == r)], g);
}
return res;
}
void dfs2(int u) {
vector<int> id;
for (int x = u; x; x = son[x])
id.emplace_back(x);
for (int u : id) {
vector<int> id;
for (auto it : G.e[u]) {
int v = it.first;
if (v != fa[u] && v != son[u])
id.emplace_back(v), dfs2(v);
}
if (id.empty())
f[u][0] = {0}, f[u][1] = {};
else {
auto res = solve1(0, id.size() - 1, id);
f[u][0] = res[0], f[u][1] = res[1];
}
}
auto res = solve2(0, id.size() - 1, id);
f[u][0] = merge(res[0][0], res[0][1]), f[u][1] = merge(res[1][0], res[1][1]);
}
signed main() {
scanf("%d", &n);
for (int i = 1; i < n; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
dfs1(1, 0), dfs2(1);
for (int i = 1; i < n; ++i) {
ll res = max(i < f[1][0].size() ? f[1][0][i] : -inf, i < f[1][1].size() ? f[1][1][i] : -inf);
if (res == -inf)
printf("? ");
else
printf("%lld ", res);
}
return 0;
}
P6326 Shopping
给出一棵树,每个点有 \(d_i\) 个喜爱度为 \(w_i\) 、价格为 \(c_i\) 的物品。你有 \(m\) 元钱,要求买的物品必须构成一个连通块,最大化喜爱度总和。
\(n \le 500\) ,\(m \le 4000\)
朴素算法是设 \(f_u\) 表示 \(u\) 子树内包含 \(u\) 的连通块的答案,而 \(f_u\) 可以通过合并所有儿子的 \(f\) 得到,具体就是决策每个子树内是留空还是选择一个含根的连通块。
对于背包问题,合并两个背包复杂度为 \(O(m^2)\) ,而加入单点的复杂度为 \(O(m)\) ,发现合并复杂度太高,考虑优化。
考虑 DFS 序配合点分治转移。先假定选出的连通块必须包含根,求出整棵树的 DFS 序,一个包含根的树上连通块的结构一定形如整棵树去掉若干个互不相交的子树,即在 DFS 序中去掉若干子段。
设 \(f_{i, j}\) 表示考虑了 dfs 序从 \(i\) 开始的后缀、容量为 \(j\) 的最大价值,转移分两种:
- 选这个点:由 \(f_{i + 1}\) 加入选当前点的信息得到转移。
- 不选这个点:那整个子树都不能选,继承 \(f_{i + siz_{u}}\) 的信息,其中 \(u\) 表示 \(i\) 代表的点。
大概原理就是把合并转化为继承和插入两个复杂度较低的操作。
而对于选出的连通块可以不包含根的问题,只要进行点分治,每次把重心作为根,算出强制包含重心的方案数,再把重心删除,递归每个连通块即可。
总时间复杂度 \(O(m n \log n)\) ,该方法也被称为树上依赖性背包。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e2 + 7, M = 4e3 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int f[N][M], w[N], c[N], d[N], siz[N], mxsiz[N], dfn[N], id[N];
bool vis[N];
int n, m, root, Siz, dfstime, ans;
void getroot(int u, int f) {
siz[u] = 1, mxsiz[u] = 0;
for (int v : G.e[u])
if (!vis[v] && v != f)
getroot(v, u), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - mxsiz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void dfs(int u, int f) {
siz[u] = 1, id[dfn[u] = ++dfstime] = u;
for (int v : G.e[u])
if (!vis[v] && v != f)
dfs(v, u), siz[u] += siz[v];
}
void solve(int u) {
vis[u] = true, dfstime = 0, dfs(u, 0);
memset(f[dfstime + 1], 0, sizeof(int) * (m + 1));
for (int i = dfstime; i; --i) {
int u = id[i];
memcpy(f[i], f[i + siz[u]], sizeof(int) * (m + 1));
for (int j = 0; j < c[u] && j <= m; ++j) {
deque<int> q;
for (int k = 0; k * c[u] + j <= m; ++k) {
auto calc = [&](int k) {
return f[i + 1][k * c[u] + j] - k * w[u];
};
while (!q.empty() && q.front() < k - d[u])
q.pop_front();
if (!q.empty())
f[i][k * c[u] + j] = max(f[i][k * c[u] + j], k * w[u] + calc(q.front()));
while (!q.empty() && calc(q.back()) <= calc(k))
q.pop_back();
q.emplace_back(k);
}
}
}
ans = max(ans, f[1][m]);
for (int v : G.e[u])
if (!vis[v])
root = 0, Siz = siz[v], getroot(v, u), solve(root);
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
G.clear(n);
for (int i = 1; i <= n; ++i)
scanf("%d", w + i);
for (int i = 1; i <= n; ++i)
scanf("%d", c + i);
for (int i = 1; i <= n; ++i)
scanf("%d", d + i);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
memset(vis + 1, false, sizeof(int) * n);
ans = root = 0, Siz = n, getroot(1, 0), solve(root);
printf("%d\n", ans);
}
return 0;
}
HDU6643 Ridiculous Netizens
给出一棵大小为 \(n\) 的树,求有多少个连通块的点权积 \(\le m\) 。
\(n \le 2000\) ,\(m \le 10^6\)
设 \(f_{i, j}\) 表示考虑了 dfs 序从 \(i\) 开始的后缀、最多还能乘 \(j\) 的最大价值,类似的外层套上点分治、内层背包转移可以做到 \(O(nm \log n)\) ,无法通过。
上述做法的瓶颈在于第二维状态过大。由于 \(\lfloor \frac{\frac{m}{x}}{y} \rfloor = \lfloor \frac{m}{xy} \rfloor\) ,根据整除分块那套理论,有效的状态只有 \(O(\sqrt{m})\) 个,因此时间复杂度可以优化到 \(O(n \sqrt{m} \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 2e3 + 7, M = 1e6 + 7;
struct Graph {
vector<int> e[N];
inline void clear(int n) {
for (int i = 1; i <= n; ++i)
e[i].clear();
}
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int f[N][N], a[N], num[N], idx[M], siz[N], mxsiz[N], id[N];
bool vis[N];
int n, m, tot, ans, root, dfstime;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec(int x, int y) {
x -= y;
if (x < 0)
x += Mod;
return x;
}
int getsiz(int u, int f) {
siz[u] = 1;
for (int v : G.e[u])
if (!vis[v] && v != f)
siz[u] += getsiz(v, u);
return siz[u];
}
void getroot(int u, int f, int Siz) {
siz[u] = 1, mxsiz[u] = 0;
for (int v : G.e[u])
if (!vis[v] && v != f)
getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]);
mxsiz[u] = max(mxsiz[u], Siz - siz[u]);
if (!root || mxsiz[u] < mxsiz[root])
root = u;
}
void dfs(int u, int f) {
siz[u] = 1, id[++dfstime] = u;
for (int v : G.e[u])
if (!vis[v] && v != f)
dfs(v, u), siz[u] += siz[v];
}
void solve(int u) {
vis[u] = true, dfstime = 0, dfs(u, 0);
memset(f[dfstime + 1] + 1, 0, sizeof(int) * tot), f[dfstime + 1][1] = 1;
for (int i = dfstime; i; --i) {
int u = id[i];
memcpy(f[i] + 1, f[i + siz[u]] + 1, sizeof(int) * tot);
for (int j = 1; j <= tot; ++j)
if (a[u] <= num[j]) {
if (idx[num[j] / a[u]] > tot)
puts("no"), exit(0);
f[i][idx[num[j] / a[u]]] = add(f[i][idx[num[j] / a[u]]], f[i + 1][j]);
}
}
for (int i = 1; i <= tot; ++i)
ans = add(ans, f[1][i]);
ans = dec(ans, 1);
for (int v : G.e[u])
if (!vis[v])
root = 0, getroot(v, u, getsiz(v, u)), solve(root);
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &m);
tot = 0;
for (int i = 1; i <= m; i = m / (m / i) + 1)
num[idx[m / i] = ++tot] = m / i;
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
G.clear(n);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
memset(vis + 1, false, sizeof(bool) * n);
ans = root = 0, getroot(1, 0, n), solve(root);
printf("%d\n", ans);
}
return 0;
}
应用
[ABC373F] Knapsack with Diminishing Values
有 \(n\) 个物品,第 \(i\) 个物品重量为 \(w_i\) ,价值为 \(v_i\) ,每种物品数量无限。
最终价值计算方法为:设第 \(i\) 个物品选了 \(k_i\) 个,则会获得 \(k_i v_i - k_i^2\) 的价值。
求背包容量为 \(m\) 时的最大价值。
\(n, m \le 3000\)
对于第 \(i\) 个物品,考虑观察第 \(k\) 次选和第 \(k - 1\) 次选的价值差:
不难发现这相当于第 \(i\) 个物品初始价值为 \(v_i - 1\) ,每选一次后价值减少 \(2\) 。
设 \(g_{i, j}\) 表示重量为 \(i\) 的物品选了 \(j\) 个的最大价值,不难用堆贪心做到 \(O(m \ln m \log n)\) 。
然后对外层做背包,设 \(f_{i, j}\) 表示考虑重量 \(\le i\) 的物品,所选重量和为 \(j\) 的最大价值,转移不难做到 \(O(m^2 \ln m)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 3e3 + 7;
priority_queue<int> q[N];
ll f[N][N], g[N][N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
int w, v;
scanf("%d%d", &w, &v);
q[w].emplace(v - 1);
}
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= m / i && !q[i].empty(); ++j) {
int v = q[i].top();
q[i].pop(), g[i][j] = g[i][j - 1] + v;
if (v - 2 > 0)
q[i].emplace(v - 2);
}
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= m; ++j)
for (int k = 0; k <= j / i; ++k)
f[i][j] = max(f[i][j], f[i - 1][j - k * i] + g[i][k]);
printf("%lld", f[m][m]);
return 0;
}
[ARC096F] Sweet Alchemy
有 \(n\) 个物品,买第 \(i\) 个物品需要 \(m_i\) 元。
除了第一个物品外,第 \(i\) 个物品有一个上级物品 \(p_i (p_i < i)\) ,所有物品构成树形结构。
给定 \(d\) ,设第 \(i\) 个物品买了 \(c_i\) 个,则要求 \(c_{p_i} \le c_i \le c_{p_i} + d\) 。
一共有 \(x\) 元,最大化购买物品的数量。
\(n \le 50\) ,\(x, d, m_i \le 10^9\)
设 \(siz_u, s_u\) 表示 \(u\) 的子树大小与 \(u\) 子树内的价格和,将第 \(i\) 个物品转化为重量为 \(w_i = s_i\) 、价值为 \(v_i = siz_u\) 、数量为 \(\begin{cases} +\infty & i = 1 \\ d & i > 1 \end{cases}\) 的物品,则问题转化为多重背包。
发现物品的重量、数量很大,但是价值却很小。一个显然的想法是将记录价值维,求解最小重量。但是总价值还是很大,仍需优化。
考虑一个经典的假贪心:按性价比 \(\frac{v_i}{w_i}\) 降序排序,贪心选取。对于这种结构可以观察到一个性质:若 \(\frac{v_i}{w_i} > \frac{v_j}{w_j}\) ,且选择了至少 \(v_i\) 个第 \(j\) 种物品,那么显然将其中 \(v_i\) 个 \(j\) 换成 \(v_j\) 个 \(i\) 更优。因为总价值均为 \(v_i v_j\) ,但是重量变小(\(w_i v_j < w_j v_i\))。
因此得到对于 \(\frac{v_i}{w_i} > \frac{v_j}{w_j}\) ,选取 \(c_i\) 个 \(i\) 时可以将 \(c_i\) 分为 \(< v_j\) 和 \(\ge v_j\) 的部分,而后者直接贪心就是对的。
因此对每个物品保留至多 \(n\) 个后做多重背包即可,此时总价值上界为 \(n^3\) ,直接二进制分组可以做到时间复杂度 \(O(n^4 \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e1 + 7;
struct Node {
ll w;
int v, id;
inline bool operator < (const Node &rhs) const {
return v * rhs.w > rhs.v * w;
}
} a[N];
ll f[N * N * N];
int fa[N];
int n, m, d;
signed main() {
scanf("%d%d%d%lld", &n, &m, &d, &a[1].w), a[1].v = 1, a[1].id = 1;
for (int i = 2; i <= n; ++i)
scanf("%lld%d", &a[i].w, fa + i), a[i].v = 1, a[i].id = i;
for (int i = n; i; --i)
a[fa[i]].w += a[i].w, a[fa[i]].v += a[i].v;
memset(f, 0x3f, sizeof(f)), f[0] = 0;
int lim = min(d, n), V = n * n * n;
for (int i = 1; i <= n; ++i) {
int k = lim;
auto insert = [&](int v, ll w) {
for (int i = V; i >= v; --i)
f[i] = min(f[i], f[i - v] + w);
};
for (int j = 0; (1 << j) <= k; ++j)
insert(a[i].v << j, a[i].w << j), k -= 1 << j;
if (k)
insert(a[i].v * k, a[i].w * k);
}
sort(a + 1, a + n + 1);
while (a[n].id != 1)
--n;
int ans = 0;
for (int i = 0; i <= V; ++i) {
if (f[i] > m)
continue;
int v = i, w = f[i];
for (int j = 1; j < n; ++j) {
int k = min<ll>(d - lim, (m - w) / a[j].w);
v += a[j].v * k, w += a[j].w * k;
}
int k = (m - w) / a[n].w;
ans = max(ans, v + a[n].v * k);
}
printf("%d", ans);
return 0;
}