2025.7.10 DP

背包问题前置基础

01背包

可滚动数组,必须倒叙枚举容量 \(j\) 否则可能选到多个相同物品. 时间复杂度 \(O(nW)\).

\[f_{i,j}=\max(f_{i-1,j}, f_{i-1, j-w_i}+v_i) \]

完全背包

可滚动数组,必须正序枚举容量 \(j\) 因为可以选多个物品. 时间复杂度 \(O(nW)\).

\[f_{i,j}=\max(f_{i,j}, f_{i, j-w_i}+v_i) \]

多重背包

直接 DP 时间复杂度是 \(O(W\sum k_i)\). 利用二进制分组可以做到 \(O(W\log \sum k_i)\);利用单调队列优化可以做到 \(O(nW)\).

\[f_{i,j}=\max_{k=0}^{k_i}(f_{i-1,j-k\times w_i}+k\times v_i) \]

混合背包

物品之间互不干扰,直接分别 DP 即可.

分组背包

遍历每个组跑 \(01\) 背包即可.

P3188 [HNOI2007] 梦幻岛宝珠

Hint:拆成题目所给的形式,对容量状压.

题目本身只是一个裸的 \(01\) 背包,但是数据范围奇大无比,所以肯定不能直接做了.

数据范围保证了每个 \(w_i\) 可以拆成 \(a\times 2^b\) 的形式,这启发我们对于每个 \(w_i\)\(a_i\)\(b_i\) 拆出来,考虑用这两个参数来刻画有关 \(w\) 的转移.

考虑状压,设 \(f_{i,j}\) 表示重量为 \(j\times 2^i\) 时的最大价值. 观察到 \(i\) 相同时可以直接转移:

\[f_{i,j}=\max_{b_k=i}(f_{i,j},f_{i,j-a_k}) \]

考虑不同的 \(i\) 怎么合并. 如果给 \(i-1\)\(k\times2^{i}\) 的重量,也就是 \((2k)\times2^{i-1}\),相当于 \(j\) 这一维有 \(2k\) 的可分配重量. 同时由于总价值 \(W\) 拆成二进制也可能有 \(2^{i-1}\) 的贡献,如果有也一并加上. 就有转移:

\[f_{i,j}=\max_{k=0}^j(f_{i-1,2k+((W>>(i-1))\&1)}+f_{i,j-k}) \]

代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 1e2 + 10, maxw = 1e3 + 10;
int n, W, w[maxn], v[maxn];
int maxb, wa[maxn], wb[maxn];
ll f[40][maxw];//n个物品容量为a,容量上界为n*a

void solve() {
    for(int i = 1; i <= n; i++) cin >> w[i] >> v[i];
    for(int i = 1; i <= n; i++) {
        for(int b = 1; ; b++) if(w[i] % (1ll << b) != 0) {wb[i] = --b; break;}
        wa[i] = w[i] / (1 << wb[i]);
    }
    
    for(int i = 1; i <= n; i++) 
        for(int a = 1000; a >= wa[i]; a--) 
            f[wb[i]][a] = max(f[wb[i]][a], f[wb[i]][a - wa[i]] + v[i]);
    maxb = 0;
    for(int s = (W >> 1); s; s >>= 1) maxb++;
    for(int b = 1; b <= maxb; b++) {
        for(int a = 1000; a >= 0; a--) {
            for(int k = 0; k <= a; k++) {
                f[b][a] = max(f[b][a], f[b][a - k] + f[b - 1][min(1000, (k << 1) + ((W & (1 << (b - 1))) != 0))]);
            }
        }
    } cout << f[maxb][1] << endl;

    return;
}
void cln() {memset(f, 0, sizeof f); return;}

int main() {
    ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);

    while(1) {
        cin >> n >> W; if(n == -1) break;
        solve(), cln();
    }

    return 0;
}

P6192 【模板】最小斯坦纳树

Hint:对关键点状压.

一个重要结论是最后选择的边集形态是一棵树,因为多余的边是无用的,有一条路径连通即可. 我们需要刻画当前关键点连通的状态,而且由于是树所以可以考虑设 \(f_{u,s}\) 表示以 \(u\) 为根,关键点与 \(u\) 的连通状态是 \(s\) 时的最小边权. 对于关键点初始时有:

\[f_{k_i,2^{i-1}=0} \]

其余状态初始赋成极大值.

考虑转移. 连通状态不改变的前提下,可以通过换根来得到其它根的答案:

\[f_{u,s}=\min(f_{u,s},f_{v,s}+w_{u,v}) \]

如果固定根不动,连通性的转移就类似于背包,需要通过枚举子集来实现.

\[f_{u,s}=\min_{s'\subset s}(f_{u,s'},f_{u,\complement_{s'}s}) \]

后者每次直接暴力枚举子集转移,但是前者直接暴力转移复杂度会非常夸张. 观察到前者的形如最短路,于是用 spfa 转移即可.

代码实现
#include<bits/stdc++.h>
using namespace std;

const int maxn = 1e2 + 10, maxm = 5e2 + 10, maxk = 1e1 + 10, inff = 1e9;
int n, m, k, ok[maxk];

int tot, head[maxn];
struct Edge{int v, nxt, w;} e[maxm << 1];
inline void add(int u, int v, int w) {e[++tot].nxt = head[u], head[u] = tot, e[tot].v = v, e[tot].w = w; return;}

queue<int> q;
int f[maxn][1 << 12]; bool inq[maxn];
void spfa(int s) {
	while(!q.empty()) {
		int u = q.front(); q.pop(), inq[u] = false;
		for(int i = head[u], v, w; i; i = e[i].nxt) {
			v = e[i].v, w = e[i].w; 
			if(w + f[u][s] < f[v][s]) {
				f[v][s] = f[u][s] + w;
				if(!inq[v]) q.push(v), inq[v] = true; 
			}
		}
	} return;
}
void init() {memset(f, 0x3f, sizeof f); return;}

int main() {
	ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
	
	cin >> n >> m >> k; init();
	for(int i = 1, u, v, w; i <= m; i++) cin >> u >> v >> w, add(u, v, w), add(v, u, w);
	for(int i = 1; i <= k; i++) cin >> ok[i], f[ok[i]][1 << (i - 1)] = 0;
	
	for(int s = 0; s < (1 << k); s++) {
		for(int i = 1; i <= n; i++) {
			for(int s1 = s & (s - 1); s1; s1 = (s1 - 1) & s) 
				f[i][s] = min(f[i][s], f[i][s1] + f[i][s ^ s1]);
			if(f[i][s] < inff) q.push(i), inq[i] = true;
		}
		spfa(s);
	} cout << f[ok[1]][(1 << k) - 1];
	
	return 0;
}

posted @ 2025-07-12 10:04  Ydoc770  阅读(14)  评论(0)    收藏  举报