9.27 动态规划测试 题解

9.27 动态规划测试 题解

A. 矩阵(合并 DP)

题意

矩阵乘法是定义在矩阵上的二元运算,支持结合律但不支持交换律。一个 \(m\times n\) 的矩阵 \(A\) 和一个 \(n\times p\) 的矩阵 \(B\) 相乘,需要进行 \(m\times n\times p\) 次乘法运算,并得到一个 \(m\times p\) 的矩阵。

现将给出行数 \(m_i\) 和列数 \(n_i\)\(N\) 个矩阵相乘,求进行乘法运算的最小次数。

\(2\le N\le500\)

思路

合并 DP,设 \(dp[i,j]\) 表示将第 \(i\) 到第 \(j\) 个矩阵合并为一个矩阵,需要进行乘法运算的最小次数。

状态转移方程为 \(dp[i,j]=\min\limits_{i\le k<j}\{dp[i,k]+dp[k+1,j]+m_i\times n_k\times n_j\}\)

时间复杂度 \(O(n^3)\)

代码

#include <iostream>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
int const N = 550;
int nn, dp[N][N], m[N], n[N];

signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	
	cin >> nn;
	f(i, 1, nn) cin >> m[i] >> n[i];
	f(p, 1, nn - 1) //枚举区间长度
		for (int i = 1, j = p + 1; j <= nn; ++i, ++j) {
			dp[i][j] = 0x3f3f3f3f; //初值
			f(k, i, j - 1)
				dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + m[i] * n[k] * n[j]);
		}
	cout << dp[1][nn] << '\n';
	
	return 0;
}

B. 时态同步(树上 DFS)

  • (题解是用 DP 做的,不过我考试时想的是 DFS)
  • 三年 OI 一场空,不开 long long 见祖宗

题意

给出一棵 \(n\) 个节点的树及其根节点 \(S\),每条边有一个通过时间 \(t_i\)。你可以进行一次「操作」,使得某条边的通过时间增加一秒。

你需要让根节点 \(S\) 到每个叶节点的所需时间都相等,且「操作」次数最少。

\(n\le 5\times 10^5,t_i\le 10^6\)

思路

容易证明:将所有叶子节点所需时间都变为最大的时间最优,并且贪心地操作深度较小的边比深度较大的边更优。

具体来说,先跑一遍 DFS,处理出边 \(i\) 下方的所有叶子节点中,所需最大的时间 \(maxx_i\),以及所有叶子节点中最大的时间 \(maxt\)

对于边 \(i\)\(maxt-maxx_i\) 即为需要「操作」的次数。由于对上方的边进行「操作」时,下方的边的 \(maxx\) 值也会发生改变,即如果设上方已经「操作」了 \(x\) 次,那么这条边只需「操作」 \(maxt-x-maxx_i\) 次。

所以在跑第二遍 DFS 的过程中,我们传入一个参数 \(remain\) 表示还需要「操作」多少次(使下方所需时间最大的叶子节点的时间变为 \(maxt\)),每次调用时 \(remain\leftarrow maxx_i\),同时 \(ans\leftarrow ans+remain-maxx_i\),即对这条边「操作」了 \(remain-maxx_i\) 次。

代码

#include <cstdio>
#include <iostream>
#include <cctype>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
#define il inline
#define int ll
using namespace std;
typedef long long ll;
int const N = 5e5 + 10;
int n, S;

int head[N], cnt;
struct Edge{
	int to, nxt, val, maxx;
} e[N << 1];
il void add(int from, int to, int val) {
	e[++cnt].to = to, e[cnt].nxt = head[from], e[cnt].val = val, head[from] = cnt;
	return;
}

int maxt;
void dfs(int x, int fa, int nw, int in_edge) {
	int maxx = 0;
	int child = 0;
	for (int i = head[x]; i; i = e[i].nxt) {
		int to = e[i].to, val = e[i].val;
		if (to == fa) continue;
		++child;
		dfs(to, x, nw + val, i);
		maxx = max(e[i].maxx, maxx);
	}
	if (!child) {
		maxt = max(maxt, nw);
		e[in_edge].maxx = nw;
		return;
	}
	if (x == S) return;
	e[in_edge].maxx = maxx;
	return;
}

int ans;
void dfs2(int x, int fa, int remain) {
	for (int i = head[x]; i; i = e[i].nxt) {
		int to = e[i].to, maxx = e[i].maxx;
		if (to == fa) continue;
		dfs2(to, x, maxx);
		ans += remain - maxx;
	}
	return;
}

signed main() {
	
    scanf("%lld%lld", &n, &S);
	int x, y, z;
	f(i, 1, n - 1) {
        scanf("%lld%lld%lld", &x, &y, &z);
		add(x, y, z), add(y, x, z);
	}
	dfs(S, -1, 0, -1);
	dfs2(S, -1, maxt);
	printf("%lld\n", ans);
	
	return 0;
}

C. 葡萄(状态压缩 DP)

题意

给定一列 \(n\) 个数 \(c_1,c_2,\dots,c_n\),现在从中取出一些数,规则是:每连续 \(k\) 个数中最少取 \(a\) 个,最多取 \(b\) 个。

现想要使取出的数的总和减掉剩余的数总和最大。求出这个最大值。

对于 \(30\%\) 的数据,\(a=0,b=k\)

对于 \(100\%\) 的数据,\(n\le 10^4,0\le a\le b\le k\le 10,|c_i|\le 10^4\)

思路

由于要使取出的数的总和减掉剩余的数总和最大,设取出的数的总和为 \(x\),所有数的总和为 \(sum\),那么答案为 \(x-(sum-x)=2x-sum\)。所以只需要最大化 \(x\) 即可。

对于 \(30\%\) 的数据,可以任意取数,所以直接贪心地取所有正数即可。

对于 \(100\%\) 的数据,注意到 \(a,b,k\) 的范围很小,考虑状态压缩 DP。

\(dp[i,sta]\) 表示当 \(c[i-k+1]\)\(c[i]\) 的状态为 \(sta\) 时,在前 \(i\) 个数中取若干数所得的最大值。

考虑如何转移状态:

显然取或者不取 \(c[i-k+1]\)\(c[i]\) 由所枚举的 \(sta\) 决定,即如果 sta & 1 == 1 那么取 \(c[i]\),否则不取。

所以我们需要讨论的只有是否取 \(c[i-k]\)

如果取,那么前一个的状态为 (sta >> 1) | (1 << (k - 1));否则为 sta >> 1

所以状态转移方程为:

dp[i][sta] = max(dp[i - 1][sta >> 1], dp[i - 1][(sta >> 1) | (1 << (k - 1))]) + c[i] * (sta & 1)

(注意 c[i] * sta & 1 一定要写在外面(这个小错误我 xx 调了一个小时

由于有 \(a,b\) 的限制,所以我们先 DFS 预处理出所有合法的 \(sta\)

然后枚举 \(sta\),暴力枚举每一位,预处理出所有的 \(dp[k,sta]\)

然后枚举 \(i\),枚举所有合法的 \(sta\),进行状态转移。

答案为 \(\max\{dp[n][sta]\}\),其中 \(sta\) 为所有合法的状态。

别忘了答案不是单纯的 \(x\),而是 \(2x-sum\)

可以顺便滚动一下数组。

时间复杂度约为 \(O(2^kn)\)

代码

#include <iostream>
#include <cstring>
#include <algorithm>
#define f(x, y, z) for (int x = (y); (x) <= (z); ++(x))
using namespace std;
int const N = 1e4 + 10;
int n, k, a, b, c[N], sum, ans;
int dp[2][1200];

int status[N], tot;
void _init(int d, int pcnt, int sta) {
	if (d >= k) {
		if (pcnt >= a && pcnt <= b)
			status[++tot] = sta;
		return;
	}
	_init(d + 1, pcnt + 1, sta ^ (1 << d));
	_init(d + 1, pcnt, sta);
	return;
}

signed main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	
	cin >> n >> k >> a >> b;
	f(i, 1, n) cin >> c[i], sum += c[i];
	if (a == 0 && b == k) { //30%
		f(i, 1, n) if (c[i] > 0) ans += c[i];
		cout << ans * 2 - sum << '\n';
		return 0;
	} else if (a == 0 && b == 0) { //特判 (竟然有20%
		cout << -sum << '\n';
		return 0;
	}
	_init(0, 0, 0);
	// sort(status + 1, status + tot + 1);
	memset(dp, 128, sizeof dp); //初值
	int nw = 1, pr = 0;
	f(i, 1, tot) {
		dp[1][status[i]] = 0;
		f(j, 0, k - 1)
			if ((status[i] >> j) & 1)
				dp[1][status[i]] += c[k - j];
	}
	f(i, k + 1, n) {
		nw ^= 1, pr ^= 1;
		memset(dp[nw], 128, sizeof dp[nw]); //初值
		f(j, 1, tot) {
			int sta = status[j];
			dp[nw][sta] = max(dp[pr][sta >> 1], dp[pr][(sta >> 1) | (1 << (k - 1))]) + c[i] * (sta & 1);
		}
	}
	ans = 0x80808080; //初值
	f(i, 1, tot) ans = max(ans, dp[nw][status[i]]);
	cout << ans * 2 - sum << '\n';
	
	return 0;
}
/*
6 4 3 4
-5 2 -4 2 -1 3

9
*/

D. 矩形

TO DO...

posted @ 2022-11-06 20:40  f2021ljh  阅读(14)  评论(0)    收藏  举报