动态规划做题记录

[JSOI2011] 柠檬

复习一下斜率优化。

首先最优方案中每一段左右端点贝壳大小显然相等,于是记 \(sum_i\)\([1,i]\) 中所有大小为 \(s_i\) 的贝壳个数。

朴素方程为 \(f_i=\max\{f_j+(sum_i-sum_{j+1}+1)^2s_i\}\)

按照斜率优化套路展开得 \(f_i=\max\)\(\{\)\(f_j+sum_{j+1}^2s_i-2sum_{j+1}s_i-2sum_isum_{j+1}s_i\)\(\}+(sum_i+2sum_i+1)s_i\)

于是可得 \(f_i=\max\{y_j-k_ix_j\}+(sum_i+2sum_i+1)s_i\)

\(y_j=f_j+sum_{j+1}^2s_j\)

\(k_i=2sum_is_i\)

\(x_j=sum_{j+1}\)

对于每种大小开一个单调栈维护上凸包即可。

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

#include <cstdio>
#include <vector>
#define int long long
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 65536, stdin), p1 == p2) ? EOF : *p1 ++)
#define X (Q[s[i + 1]].back())
#define Y (Q[s[i + 1]][Q[s[i + 1]].size() - 2])

char buf[65536], *p1, *p2;
inline int max(const int x, const int y) {return x > y ? x : y;}
inline int sqr(int x) {return x * x;}
inline int read() {
	char ch;
	int x = 0;
	while ((ch = gc) < 48);
	do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
	return x; 
}

int s[100005], sum[100005], lst[10005], dp[100005];
std::vector<int> Q[10005];
inline int calc(int j, int i) {return dp[j] + sqr(sum[i] - sum[j + 1] + 1) * s[i];}
inline double slope(int j, int i) {
	return (dp[i] + sqr(sum[i + 1]) * s[i + 1] - dp[j] - 1.0 * sqr(sum[j + 1]) * s[j + 1]) / (1.0 * sum[i + 1] - sum[j + 1]);
}

signed main() {
	int n = read(), ans = 0ll;
	for (int i = 1; i <= n; ++ i) sum[i] = sum[lst[s[i] = read()]] + 1, lst[s[i]] = i;
	for (int i = 1; i <= n; ++ i) {
		while (Q[s[i]].size() > 1 && calc(Q[s[i]].back(), i) <= calc(Q[s[i]][Q[s[i]].size() - 2], i)) Q[s[i]].pop_back();
		if (Q[s[i]].size()) dp[i] = calc(Q[s[i]].back(), i);
		ans = max(ans, dp[i] = max(dp[i], calc(0, i)));
		while (Q[s[i + 1]].size() > 1 && slope(X, Y) <= slope(Y, i)) Q[s[i + 1]].pop_back();
		Q[s[i + 1]].push_back(i);
	}
	printf("%lld", ans);
	return 0;
}

区间

给定 \(n\) 段区间 \([l,r]\)

从中选择若干段(至少 \(2\) 段)区间,最大化它们的交集长度与并集长度的乘积 。

\(1\le n\le 10^6,1\le l_i<r_i\le 10^6\)

解法:首先把所有线段按照左端点排序,去除被包含的线段。

然后显然只会选两条线段,设 \([1,i-1]\) 中选择 \(i,j\) 这两条线段的最大答案是 \(f_i\),注意到 \(f_i\) 具有决策单调性,且是单调递增的。

虽然这连个 dp 都算不上,但是仍然可以用 dp 方程的定理来证明它是决策单调递增的。

硬写成 dp 方程就是 \(f_i=max\{w(j,i)\}\)\(w(j,i)\) 就是选择线段 \(i,j\) 的答案,由于是 \(\max\),所以只需证交叉大于包含即可。

\(a<b<c<d\),则需要证明 \(w(a,d)+w(b,c)-w(a,c)-w(b,d)\ge 0\),暴力展开即可(其实推到这一步正确性已经比较显然,可以直接感性理解了)。

时间复杂度 \(O(n\log n)\)

#include <cstdio>
#include <algorithm>
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 65536, stdin), p1 == p2) ? EOF : *p1 ++)

char buf[65536], *p1, *p2;
inline int read() {
	char ch;
	int x = 0;
	while ((ch = gc) < 48);
	do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
	return x;
}

struct Line {
	int l, r;
	inline bool operator < (const Line a) const {return l < a.l;}
} a[1000005];
int f[1000005], l[1000005], r[1000005];
long long ans;
long long calc(int i, int j) {return (1ll * r[j] - l[i]) * (r[i] - l[j]);}
void solve(int l, int r, int x, int y) {
	int mid = l + r >> 1;
	long long now = -1e18;
	for (int i = x; i < mid && i <= y; ++ i)
		if (calc(i, mid) > now) now = calc(i, mid), f[mid] = i;
	if (l < mid) solve(l, mid - 1, x, f[mid]);
	if (mid < r) solve(mid + 1, r, f[mid], y);
}

int main() {
	int n = read(), m;
	for (int i = 1; i <= n; ++ i) a[i].l = read(), a[i].r = read();
	std::sort(a + 1, a + n + 1);
	l[m = 1] = a[1].l, r[1] = a[1].r;
	for (int i = 2; i <= n; ++ i) {
		if (a[i].r <= r[m]) {ans = std::max(ans, (1ll * a[i].r - a[i].l) * (r[m] - l[m])); continue;}
		if (a[i].l == l[m]) ans = std::max(ans, (1ll * a[i].r - a[i].l) * (r[m] - l[m])), -- m;
		l[++ m] = a[i].l, r[m] = a[i].r;
	}
	solve(1, m, 1, m);
	for (int i = 2; i <= m; ++ i) ans = std::max(ans, calc(f[i], i));
	printf("%lld", ans);
	return 0;
}

「是男人就过8题——Pony.ai」SignLocation

首先根据直觉瞎猜一个结论,标志一定放在车站上一定可以得到最优解,证明可以考虑类似绝对值差最小的证明。

然后 \(f_{i,j}\) 表示当前安了 \(i\) 个标志,最后一个安在 \(j\) 位置,\(\sum\limits^j_{x=1}\sum\limits_{x\ne y}c(x,y)\) 的最小值。

转移为 \(f_{i,j}=\min\{f_{i-1,k}+w(k,i)\}\),其中 \(w(k,i)\) 是一个贼难算需要维护一堆前缀和的东西。

然后盲猜它有决策单调性就完了

#include <cstdio>
#define int long long

inline int min(const int x, const int y) {return x < y ? x : y;}
int dp[205][10005], a[10005], s1[10005], s2[10005], s3[10005], n, k;

inline int getsum(int l, int r) {
	if (l > r) return 0;
	return s3[l] - s3[r + 1] - (s1[r] - s1[l - 1]) * (n - r);
}
inline int cost(int j, int i) {
	int sum = s1[i - 1] - s1[j];
	return (s2[i - 1] - s2[j] - sum * (j + 1) - getsum(j + 1, i - 2)) * 2 +
	sum * (j - (n - i + 1)) - a[j] * j * (i - j - 1) + a[i] * (n - i + 1) * (i - j - 1);
}
void solve(int l, int r, int x, int y, int cur) {
	int mid = l + r >> 1, k = 0;
	for (int i = x; i <= y && i < mid; ++ i)
		if (dp[cur][mid] > dp[cur - 1][i] + cost(i, mid))
			k = i, dp[cur][mid] = dp[cur - 1][i] + cost(i, mid);
	if (l < mid) solve(l, mid - 1, x, k, cur);
	if (mid < r) solve(mid + 1, r, k, y, cur);
}

signed main() {
	while (scanf("%lld%lld", &n, &k) == 2) {
		if (k > n) k = n;
		for (int i = 0; i <= k; ++ i)
		for (int j = 0; j <= n; ++ j) dp[i][j] = 1e18;
		dp[0][0] = 0;
		for (int i = 1; i <= n; ++ i) scanf("%lld", a + i);
		for (int i = 1; i <= n; ++ i) s1[i] = s1[i - 1] + a[i], s2[i] = s2[i - 1] + a[i] * i;
		s3[n + 1] = 0;
		for (int i = n; i; -- i) s3[i] = s3[i + 1] + a[i] * (n - i + 1);
		for (int i = 1; i <= k; ++ i) solve(0, n, 0, n, i);
		int ans = 1e18;
		for (int i = k; i <= n; ++ i) ans = min(ans, dp[k][i] + cost(i, n + 1));
		printf("%lld\n", ans);
	}
	return 0;
}

记忆的轮廓

首先考虑当前只有 \(l,r(l<r)\) 是存档点,从 \(l\)\(r\) 期望步数是多少。

假设 \(P_u\) 为从 \(l\) 出发到达 \(u\) 的概率,\(dep_u\)\(u\)\(l\) 的距离,那么期望步数为

\[w(l,r)=\sum P_i(dep_i+1+w_{l,r})-P_rw(l,r)-P_r \]

其中 \(i\)\([l,r)\) 子树内的错误节点。

于是解方程后 \(w\) 可以暴力 \(O(n^2)\) 预处理出。

\(f_{i,j}\) 为在前 \(j\) 个节点设置了 \(i\) 个存档点,最后一个存档点为 \(i\)

\[f_{i,j}=\min\{f_{i-1,k}+w(k,j)\} \]

显然这个式子满足决策单调性,使用整体二分可以 \(O(n^2\log n)\) 解决。

#include <cstdio>
#include <vector>

inline double min(const double x, const double y) {return x < y ? x : y;}
std::vector<int> G[2205];
double dp[2205][2205], w[2205][2205], P[2205], sum[2205];
int dep[2205], n, m, p;

void dfs(int u) {
	bool isleaf = true;
	for (int v : G[u]) {
		isleaf = false;
		dep[v] = dep[u] + 1, P[v] = P[u] / (G[u].size() + (u < n));
		dfs(v), sum[u] += sum[v];
	}
	if (u < n) {
		dep[u + 1] = dep[u] + 1, P[u + 1] = P[u] / (G[u].size() + (u < n));
		sum[u + 1] = sum[u], dfs(u + 1);
	}
	if (n < u && isleaf) sum[u] = P[u] * dep[u];
}
void solve(int l, int r, int x, int y, int cur) {
	int mid = l + r >> 1, k = 0;
	for (int i = x; i < mid && i <= y; ++ i)
		if (dp[cur][mid] > dp[cur - 1][i] + w[i][mid])
			dp[cur][mid] = dp[cur - 1][i] + w[i][mid], k = i;
	if (l < mid) solve(l, mid - 1, x, k ? k : y, cur);
	if (mid < r) solve(mid + 1, r, k ? k : x, y, cur);
}

int main() {
	int _;
	scanf("%d", &_);
	while (_ --) {
		scanf("%d%d%d", &n, &m, &p);
		for (int i = 1; i <= m; ++ i) G[i].clear();
		for (int i = 1, u, v; i <= m - n; ++ i) scanf("%d%d", &u, &v), G[u].push_back(v);
		for (int i = 1; i < n; ++ i) {
			for (int i = 1; i <= m; ++ i) sum[i] = P[i] = 0;
			dep[i] = 1, P[i] = 1, dfs(i);
			for (int j = i + 1; j <= n; ++ j) w[i][j] = (sum[j - 1] + P[j] * (dep[j] - 1)) / P[j];
		}
		for (int i = 0; i <= p; ++ i)
			for (int j = 0; j <= n; ++ j) dp[i][j] = 1e18;
		dp[1][1] = 0;
		for (int i = 2; i <= p; ++ i) solve(1, n, 1, n, i);
		printf("%.4lf\n", dp[p][n]);
	}
	return 0;
}

邮局问题 加强版

就是 IOI2000 把数据范围开到了 5e5。

首先,刚好建立 \(m\) 个邮局这种限制显然直接套个 wqs 二分上去削掉一维。

然后 \(f_i\) 为前 \(i\) 个村庄离它们最近的邮局加上 wqs 二分出来的建站花费最小值。

\(f_i=\min\{f_j+w(j,i)+val\}\),其中 \(val\) 为 wqs 二分出来的值。

显然这个方程具有决策单调性,二分队列优化。

\(w(j,i)\) 可以稍微预处理一下后 \(O(1)\) 计算。

#include <cstdio>
#include <algorithm>
#include <queue>
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 65536, stdin), p1 == p2) ? EOF : *p1 ++)

typedef long long ll;
char buf[65536], *p1, *p2;
inline int min(const int x, const int y) {return x < y ? x : y;}
inline int min(const ll x, const ll y) {return x < y ? x : y;}
inline int max(const int x, const int y) {return x > y ? x : y;}
inline int read() {
	char ch;
	int x = 0;
	while ((ch = gc) < 48);
	do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
	return x;
}

int a[500005], n, m;
ll s[500005], f[500005], g[500005], belong[1000005], hd, tl;
struct node {int j, l, r;} q[500005];

inline ll cost(int l, int r) {
	if (!l) return 1ll * a[r] * r - s[r];
	int k = belong[a[l] + a[r] >> 1];
	return 1ll * a[r] * (r - k) - s[r] + (s[k] << 1) - s[l - 1] - 1ll * a[l] * (k - l + 1);
}
inline bool comp(const ll f1, const int g1, const ll f2, const int g2) {
	return f1 ^ f2 ? f1 < f2 : g1 <= g2;
}

std::pair<ll, int> check(ll val) {
	q[hd = tl = 1] = node{0, 1, n};
	std::pair<ll, int> res(1e18, 0);
	for (int i = 1; i <= n; ++ i) {
		while (hd <= tl && q[hd].r < i) ++ hd;
		q[hd].l = max(q[hd].l, i + 1);
		f[i] = f[q[hd].j] + cost(q[hd].j, i) + val, g[i] = g[q[hd].j] + 1;
		if (q[hd].l > q[hd].r) ++ hd;
		while (hd <= tl && comp(f[i] + cost(i, q[tl].l), g[i], f[q[tl].j] + cost(q[tl].j, q[tl].l), g[q[tl].j])) -- tl;
		if (hd > tl) q[++ tl] = node{i, i + 1, n};
		else {
			int l = q[tl].l, r = q[tl].r;
			while (l < r) {
				int mid = l + r + 1 >> 1;
				if (comp(f[i] + cost(i, mid), g[i], f[q[tl].j] + cost(q[tl].j, mid), g[q[tl].j])) r = mid - 1;
				else l = mid;
			}
			q[tl].r = l;
			if (l < n) q[++ tl] = node{i, l + 1, n};
		}
		if (comp(f[i] + s[n] - s[i] - 1ll * a[i] * (n - i), g[i], res.first, res.second))
			res.first = f[i] + s[n] - s[i] - 1ll * a[i] * (n - i), res.second = g[i];
	}
	return res;
}

int main() {
	n = read(), m = read();
	for (int i = 1; i <= n; ++ i) a[i] = read();
	std::sort(a + 1, a + n + 1);
	for (int i = 1; i <= n; ++ i) s[i] = s[i - 1] + a[i];
	for (int i = 1; i < n; ++ i) 
		for (int j = a[i]; j < a[i + 1]; ++ j) belong[j] = i;
	belong[a[n]] = n;
	ll l = 0, r = 1e10, ans = 0;
	while (l < r) {
		long long mid = l + r >> 1;
		std::pair<ll, int> tmp = check(mid);
		if (tmp.second <= m) ans = tmp.first - m * mid, r = mid - 1;
		else l = mid + 1;
	}
	printf("%lld", ans);
	return 0;
}

「ROI 2019 Day2」课桌

首先,把每个班学生按身高排序,排在相邻位置两个学生肯定坐同一张课桌。不妨把 \(1,2\) 分为一组,\(3,4\) 分为一组...\(2n-1,2n\) 分为一组。

然后,把每个课桌按左端点排序,去掉被完全包含的课桌,这样课桌的左右端点都是递增的。

假设给第 \(i\) 组学生安排第 \(j\) 个课桌,则 \(j\) 随着 \(i\) 的增大而增大,整体二分即可。

#include <cstdio>
#include <algorithm>
#define gc (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 65536, stdin), p1 == p2) ? EOF : *p1 ++)

char buf[65536], *p1, *p2;
inline int max(const int x, const int y) {return x > y ? x : y;}
inline int read() {
	char ch;
	int x = 0;
	while ((ch = gc) < 48);
	do x = x * 10 + ch - 48; while ((ch = gc) >= 48);
	return x;
}

struct Line {
	int l, r;
	inline bool operator < (const Line a) const {return l < a.l;}
} seg[200005], b[200005];
int *a[200005], *c[200005], f[200005], n, m, k;
long long *s[200005];
long long Ans;

inline int calc(int i, int j) {return max(0, max(i - seg[j].r, seg[j].l - i));}

void solve(int l, int r, int x, int y) {
	int mid = l + r >> 1;
	long long ans = 1e18;
	for (int i = x; i <= y; ++ i) {
		long long sum = 0ll;
		int p = std::lower_bound(c[mid] + 1, c[mid] + 2 * m + 1, seg[i].l) - c[mid] - 1;
		sum += 1ll * seg[i].l * p - s[mid][p];
		p = std::upper_bound(c[mid] + 1, c[mid] + 2 * m + 1, seg[i].r) - c[mid] - 1;
		sum += s[mid][2 * m] - s[mid][p] - 1ll * seg[i].r * (2 * m - p);
 		if (sum < ans) ans = sum, f[mid] = i;
	}
	Ans += ans;
	if (l < mid) solve(l, mid - 1, x, f[mid] ? f[mid] : y);
	if (mid < r) solve(mid + 1, r, f[mid] ? f[mid] : x, y);
}

signed main() {
	m = read(), n = read(), k = read();
	for (int i = 0; i <= m; ++ i) a[i] = new int[2 * n + 5];
	for (int i = 0; i <= n; ++ i) c[i] = new int[2 * m + 5], s[i] = new long long[2 * m + 5];
	for (int i = 1; i <= k; ++ i) b[i].l = read(), b[i].r = read();
	for (int i = 1; i <= m; ++ i) {
		for (int j = 1; j <= 2 * n; ++ j) a[i][j] = read();
		std::sort(a[i] + 1, a[i] + 2 * n + 1);
	}
	for (int i = 1; i <= m; ++ i)
		for (int j = 1; j <= 2 * n; ++ j) c[j + 1 >> 1][j % 2 * m + i] = a[i][j];
	for (int i = 1; i <= n; ++ i) {
		std::sort(c[i] + 1, c[i] + 2 * m + 1);
		s[i][0] = 0ll;
		for (int j = 1; j <= 2 * m; ++ j) s[i][j] = s[i][j - 1] + c[i][j];
	}
	std::sort(b + 1, b + k + 1);
	int now = 0;
	for (int i = 1; i <= k; ++ i) {
		if (b[i].r <= seg[now].r) continue;
		if (b[i].l == seg[now].l) seg[now] = b[i];
		else seg[++ now] = b[i];
	}
	solve(1, n, 1, now);
	printf("%lld\n", Ans);
	return 0;
}
posted @ 2022-03-27 17:35  zqs2020  阅读(43)  评论(0编辑  收藏  举报