dp动态规划

背包

多重背包的单调队列优化

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[i1][jkwi]+kvi}
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[jwi],f[j2×wi],f[j3×wi]转移而来

因此,对第 i i i个物品,按照重量 w i w_i wi将枚举的变量 j j j分成块。例如 w = 3 w=3 w=3时,有

j01234567
j   m o d   w i j\,mod\,w_i jmodwi01201201

将同余的拿出来编号,假设余数为 d d d,最多能分为 W − d w i \frac{W-d}{w_i} wiWd组,有:

编号j01234
对应体积dd+wd+2wd+3wd+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]+(jk)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]kvi}+jvi
单调队列维护窗口大小为 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[k1][mw]+v,选该物品选该组之前的物品 d p [ k ] [ m − w ] + v dp[k][m-w]+v dp[k][mw]+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[k1][mw]+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 Si<<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 ST
  • 集合S和T的交集: S ∪ T S\cup T ST

遍历子集

遍历集合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][ij]+jw

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]);
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值