背包
多重背包的单调队列优化
N个物品,容量为W
每件物品的重量为 w i w_i wi,价值为 v i v_i vi,数量为 m i m_i mi个。一般转移方程为
d p [ i ] [ j ] = m a x { d p [ i ] [ j ] , d p [ i − 1 ] [ j − k ∗ w i ] + k ∗ v i } dp[i][j]=max\{dp[i][j],dp[i-1][j-k*w_i]+k*v_i\} dp[i][j]=max{dp[i][j],dp[i−1][j−k∗wi]+k∗vi}
f [ j ] f[j] f[j]只能由 f [ j − w i ] , f [ j − 2 × w i ] , f [ j − 3 × w i ] … f[j-w_i],f[j-2\times w_i],f[j-3\times w_i]\dots f[j−wi],f[j−2×wi],f[j−3×wi]…转移而来因此,对第 i i i个物品,按照重量 w i w_i wi将枚举的变量 j j j分成块。例如 w = 3 w=3 w=3时,有
j 0 1 2 3 4 5 6 7 j m o d w i j\,mod\,w_i jmodwi 0 1 2 0 1 2 0 1 将同余的拿出来编号,假设余数为 d d d,最多能分为 W − d w i \frac{W-d}{w_i} wiW−d组,有:
编号j 0 1 2 3 4 对应体积 d d+w d+2w d+3w d+4w 数量m的限制转化为窗口大小的限制,即每个编号只有由前m个转移过来。
因此,转移方程
d
p
[
j
]
=
m
a
x
{
d
p
[
k
]
+
(
j
−
k
)
v
i
}
dp[j]=max\{dp[k]+(j-k)v_i\}
dp[j]=max{dp[k]+(j−k)vi},改写为单调队列可优化形式:
d
p
[
j
]
=
m
a
x
{
d
p
[
k
]
−
k
∗
v
i
}
+
j
∗
v
i
dp[j]=max\{dp[k]-k*v_i\}+j*v_i
dp[j]=max{dp[k]−k∗vi}+j∗vi
单调队列维护窗口大小为
m
i
m_i
mi的递减序列。
#define pr pair<int, int>
int dp[maxm], W, n;
pr q[maxm]; int head, tail;
void push(pr x) {
while (tail >= head && x.second >= q[tail].second)
tail--;
tail++;
q[tail] = x;
}
int main()
{
while (~scanf("%d%d", &W, &n)) {
memset(dp, 0x3f, sizeof(dp)); dp[0] = 0;
for (int i = 0; i < n; ++i) {
int m, w, v; scanf("%d%d%d", &m, &w, &v);
for (int d = 0; d < w; ++d) {
head = 0, tail = -1;
for (int j = 0; j <= (W - d) / w; ++j) {
push(pr(j, dp[j * w + d] - j * v));
if (q[head].first < j - m) head++;
dp[j * w + d] = q[head].second + j * v;
}
}
}
printf("%d\n", dp[W]);
}
}
分组背包
k k k组物品,每组由一些物品,物品有容量和价值
每组至少选一个物品
定义状态: d p [ k ] [ m ] dp[k][m] dp[k][m]表示第 k k k组及以前容量为 m m m能获得的最大价值
状态转移:不选该物品则选该组之前的物品 d p [ k ] [ m ] dp[k][m] dp[k][m],选该物品不选该组之前的物品 d p [ k − 1 ] [ m − w ] + v dp[k-1][m-w]+v dp[k−1][m−w]+v,选该物品选该组之前的物品 d p [ k ] [ m − w ] + v dp[k][m-w]+v dp[k][m−w]+v
for (int j = 0, t, w; j < m; ++j) {
scanf("%d%d", &t, &w);
for (int k = T; k >= t; --k) {
if (~dp[i][k - t]) dp[i][k] = max(dp[i][k], dp[i][k - t] + w);
if (~dp[i - 1][k - t]) dp[i][k] = max(dp[i][k], dp[i - 1][k - t] + w);
}
}
每组最多选一个物品
定义状态: d p [ k ] [ m ] dp[k][m] dp[k][m]表示第 k k k组及以前容量为 m m m能获得的最大价值
状态转移:不选这个物品则选该组之前的物品 d p [ k ] [ m ] dp[k][m] dp[k][m],选这个物品则不选之前的物品 d p [ k − 1 ] [ m − w ] + v dp[k-1][m-w]+v dp[k−1][m−w]+v
for (int k = T; k >= 0; --k)
dp[i][k] = dp[i - 1][k];
for (int j = 0, t, w; j < m; ++j) {
scanf("%d%d", &t, &w);
for (int k = T; k >= t; --k) if (~dp[i - 1][k - t]) {
dp[i][k] = max(dp[i][k], dp[i - 1][k - t] + w);
}
}
每组没限制
对每个物品做01背包
for (int k = T; k >= 0; --k)
dp[i][k] = dp[i - 1][k];
for (int j = 0, t, w; j < m; ++j) {
scanf("%d%d", &t, &w);
for (int k = T; k >= t; --k) if (~dp[i][k - t]) {
dp[i][k] = max(dp[i][k], dp[i][k - t] + w);
}
}
队列模拟
使用队列保存每个阶段的状态值,进行一定的剪枝以降低复杂度。
例题
在每个联通块内做01背包,每个联通块内有一系列物品,拥有容量和时间,求单个联通块内容量大于m的最少时间。
对每个联通块,用一个普通队列保存每个物品选和不选后的状态,再用一个优先队列剪枝掉容量小时间大的无用状态。
const int maxn = 50 + 5;
vector<int> E[maxn];
struct node
{
int t, g;
bool operator <(const node& rhs) const {
if (g != rhs.g) return g < rhs.g;
return t > rhs.t;
}
} s;
int t[maxn], g[maxn];
vector<node> block[maxn];
int vis[maxn];
void dfs(int u, int id) {
vis[u] = 1; block[id].push_back((node){t[u], g[u]});
for (auto e : E[u]) {
if (vis[e]) continue;
dfs(e, id);
}
}
priority_queue<node> q2;
queue<node> q1;
int main()
{
int T; scanf("%d", &T);
for (int cas = 1; cas <= T; ++cas) {
int n, m; scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
E[i].clear();
block[i].clear();
}
for (int i = 1, k, x; i <= n; ++i) {
scanf("%d%d%d", &t[i], &g[i], &k);
for (int j = 0; j < k; ++j) {
scanf("%d", &x);
E[i].push_back(x);
}
}
int tot = 0; memset(vis, 0, sizeof(vis));
for (int i = 1; i <= n; ++i) {
if (!vis[i]) {
dfs(i, ++tot);
}
}
int ans = 0x3f3f3f3f;
for (int i = 1; i <= tot; ++i) {
while (!q1.empty()) q1.pop();
while (!q2.empty()) q2.pop();
s.t = s.g = 0;
q1.push(s);
for (int j = 0; j < block[i].size(); ++j) {
while (!q1.empty()) {
s = q1.front(); q1.pop();
q2.push(s);
s.t += block[i][j].t;
s.g += block[i][j].g;
if (s.g >= m) {
ans = min(ans, s.t);
continue;
}
if (s.t >= ans) continue;
q2.push(s);
}
int minT = 0x3f3f3f3f;
while (!q2.empty()) {
s = q2.top(); q2.pop();
if (s.t < minT) {
q1.push(s); minT = s.t;
}
}
}
}
printf("Case %d: ", cas);
if (ans == 0x3f3f3f3f)
puts("Poor Magina, you can't save the world all the time!");
else
printf("%d\n", ans);
}
}
拓扑排序
有向无环图中对每个物品做dp,可以处理出拓扑序,因为序中前面的物品与后面的物品没有关系,因此按拓扑序将当前物品的状态转移到有边相连的物品状态上
例题
有向无环图中有n个节点,每个节点都有一个物品拥有容量和价值,物品数量无限,每条边有长度,经过一条边的代价是花费 已用背包容量*长度 的能量,求取得最大价值时的最少能量花费
定义 d p [ u ] [ i ] dp[u][i] dp[u][i]表示u节点下花费背包容量i下取得的最大价值, d p 2 [ u ] [ i ] dp2[u][i] dp2[u][i]表示u节点下花费背包容量i下取得最大价值下的最少能量花费。处理出拓扑序后,按序遍历更新相连的点,更新按完全背包。
const int inf = 0x3f3f3f3f;
const int maxn = 600 + 5;
const int maxm = 2000 + 5;
int w[maxn], V[maxn];
vector<pair<int, int> >E[maxn];
int n, m, W, x, in[maxn];
int vis[maxn], dp[maxn][maxm], dp1[maxn][maxm];
int top_arr[maxn], top_tot;
stack<int> s;
void top() {
top_tot = 0;
for (int i = 1; i <= n; ++i) {
if (!in[i]) s.push(i);
while (s.size()) {
int u = s.top(); s.pop();
top_arr[++top_tot] = u;
for (auto e : E[u]) {
in[e.first]--;
if (!in[e.first]) s.push(e.first);
}
}
}
}
int main()
{
while (~scanf("%d%d%d%d", &n, &m, &W, &x)) {
for (int i = 1; i <= n; ++i) {
scanf("%d%d", &w[i], &V[i]);
E[i].clear(); in[i] = vis[i] = 0;
}
for (int i = 0; i < m; ++i) {
int u, v, l; scanf("%d%d%d", &u, &v, &l);
E[u].push_back(make_pair(v, l));
in[v]++;
}
top();
//initial
memset(dp, 0, sizeof(dp));
memset(dp1, -1, sizeof(dp1));
int mx_val = 0, mn_egy = 0;
for (int i = 0; i <= W; ++i) {
dp1[x][i] = 0;
if (i >= w[x])
dp[x][i] = max(dp[x][i], dp[x][i - w[x]] + V[x]);
mx_val = max(mx_val, dp[x][i]);
}
vis[x] = 1;
for (int i = 1; i <= n; ++i) {
int u = top_arr[i]; if (!vis[u]) continue;
for (auto e : E[u]) {
int v = e.first, l = e.second; vis[v] = 1;
for (int j = 0; j <= W; ++j) {
if (dp[v][j] < dp[u][j]) {
dp[v][j] = dp[u][j];
dp1[v][j] = dp1[u][j] + l * j;
}
else if (dp[v][j] == dp[u][j]) {
if (dp1[v][j] == -1) dp1[v][j] = dp1[u][j] + l * j;
else dp1[v][j] = min(dp1[v][j], dp1[u][j] + l * j);
}
if (j && dp[v][j] == dp[v][j - 1])
dp1[v][j] = min(dp1[v][j], dp1[v][j - 1]);
}
for (int j = w[v]; j <= W; ++j) {
if (dp[v][j] < dp[v][j - w[v]] + V[v]) {
dp[v][j] = dp[v][j - w[v]] + V[v];
dp1[v][j] = dp1[v][j - w[v]];
}
else if (dp[v][j] == dp[v][j - w[v]] + V[v]) {
dp1[v][j] = min(dp1[v][j], dp1[v][j - w[v]]);
}
}
for (int j = 0; j <= W; ++j) {
if (dp[v][j] > mx_val) {
mx_val = dp[v][j];
mn_egy = dp1[v][j];
}
else if (dp[v][j] == mx_val) {
mn_egy = min(mn_egy, dp1[v][j]);
}
}
}
}
printf("%d\n", mn_egy);
}
}
状态压缩
集合构造
- 判断第 i i i个元素是否属于集合S: i f ( S > > i & 1 ) if(S>>i\&1) if(S>>i&1)
- 向集合S加入第 i i i个元素 S ∪ { i } S\cup\{i\} S∪{i}: S ∣ i < < i S|i<<i S∣i<<i
- 从集合中去除第 i i i个元素 S ∣ { i } S|\{i\} S∣{i}: S & ∼ ( 1 < < i ) S\&\sim(1<<i) S&∼(1<<i)
- 集合S和T的并集: S ∣ T S|T S∣T
- 集合S和T的交集: S ∪ T S\cup T S∪T
遍历子集
遍历集合S的所有子集i,如S=101,{i}=101,100,001,000
for (int i = (S - 1) & S; i; i = (i - 1) & i);
旅行商问题
给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短回路
定义 d p [ s ] [ u ] dp[s][u] dp[s][u]表示已经经过的城市的集合以u为终点的最短路径
//多个旅行商问题
memset(dp, 0x3f, sizeof(dp));
memset(_dp, 0x3f, sizeof(_dp));
_dp[1][0] = 0;
//处理每个团
for (int i = 0; i < sz; ++i) if (tag[i]) {
//遍历团中的每个结点作为终点
for (int j = 0; j < n; ++j) if ((i >> j) & 1) {
dp[i] = min(dp[i], _dp[i][j] + dis[j][0]);
//遍历团外的点走到终点
for (int k = 0; k < n; ++k) if (!((i >> k) & 1)) {
_dp[i | (1 << k)][k] = min(_dp[i | (1 << k)][k], _dp[i][j] + dis[j][k]);
}
}
}
for (int i = 0; i < sz; ++i) {
for (int j = (i - 1) & i; j; j = (j - 1) & i) {
//遍历子集,并且构造出包含教练(0号点)的情况
dp[i] = min(dp[i], dp[j | 1] + dp[(i - j) | 1]);
}
}
printf("%d %d\n", ans, dp[sz - 1]);
树上背包
将背包问题转移到树上,用子树状态来更新父节点的状态,有两种方式:边遍历边更新吗,遍历完再更新。
例题
在一棵树上,起始点s有k个机器人,经过每条边有代价,求遍历完所有节点的代价最小值
定义状态
d
p
[
u
]
[
k
]
dp[u][k]
dp[u][k]表示u节点有k个机器人没有回来,特别的
d
p
[
u
]
[
0
]
dp[u][0]
dp[u][0]表示所有机器人都回到u节点。状态转移方程即为
d
p
[
u
]
[
i
]
=
d
p
[
v
]
[
j
]
+
d
p
[
u
]
[
i
−
j
]
+
j
∗
w
dp[u][i]=dp[v][j]+dp[u][i-j]+j*w
dp[u][i]=dp[v][j]+dp[u][i−j]+j∗w
const int maxn = 1e4 + 5;
const int maxm = 10 + 5;
vector< pair<int, int> > E[maxn];
int dp[maxn][maxm], n, s, k;
void dfs(int u, int fa) {
for (auto e : E[u]) {
int v = e.first, w = e.second;
if (v == fa) continue;
dfs(v, u);
for (int i = k; i >= 0; --i) {
//对该子树情况,初始化j=0的情况
dp[u][i] += dp[v][0] + 2 * w;
for (int j = 1; j <= i; ++j) {
dp[u][i] = min(dp[u][i], dp[u][i - j] + dp[v][j] + w * j);
}
}
}
}
int main()
{
while (~scanf("%d%d%d", &n, &s, &k)) {
for (int i = 1; i <= n; ++i)
E[i].clear();
for (int i = 1; i < n; ++i) {
int u, v, w; scanf("%d%d%d", &u, &v, &w);
E[u].push_back(make_pair(v, w));
E[v].push_back(make_pair(u, w));
}
memset(dp, 0, sizeof(dp));
dfs(s, 0);
printf("%d\n", dp[s][k]);
}
}