P2467 地精部落 和 P2885 [USACO07NOV]Telephone Wire G 题解

P2467 SDOI2010地精部落

称满足条件的序列为"波动序列"
性质1: 如果一个波动序列中 i 和 i+1 不相邻, 则交换这两个数后仍然是波动序列
性质2: 将一个波动序列反转, 翻转后仍然是波动序列
设 f[i][j] 表示由 1~i 的数构成的波动序列, 第一个数为 j 且为山峰的方案数
如果 j 与 j-1 不相邻, 则交换这两个数后仍然满足是波动序列: f[i][j]←f[i][j-1]
如果 j 与 j-1 相邻, 则 j-1 必定为山谷, 且位于 [2,i] 的数在 [1,j)∪(j,i] 中
此时将 (j,i] 中的数均减 1 就变成将 i-1 个数排成第一个数 j-1 为山谷的方案数
将这个序列反转 (每个数变为i减去自己) 得到第一个数为 i-j+1 的方案数

点击查看代码
#include <stdio.h>
const int N = 4205;
int n, mod, f[2][N], res;
int main() {
	scanf("%d%d", &n, &mod);
	f[0][2] = 1; // f[2][2] = 1: [2,1]
	for(int i = 3; i <= n; i ++)
		for(int j = 2; j <= i; j ++)
			f[i&1][j] = (f[i&1][j-1] + f[(i-1)&1][i-j+1]) % mod;
	for(int i = 2; i <= n; i ++) (res += f[n&1][i]) %= mod;
	printf("%d\n", (res << 1) % mod);
	return 0;
}

USACO07NOVTelephone Wire G
给出若干棵树的高度,你可以进行一种操作:把某棵树增高h,花费为hh。
操作完成后连线,两棵树间花费为高度差
定值c。
求两种花费加和最小值。
朴素DP:设 f[i][j] 表示处理前 i 个树, 将第 i 个树的高度变为 j 的最小花费
转移: f[i][j] <- f[i-1][k] +|j-k|*c + (j-h[i])*(j-j[i]) 其中 j>=h[i]k>=h[i-1]
时间复杂度 \(O(n (\max h)^2)\)

  • 两种优化
    第一种: 注意到 \(j\) 单调递增时 \(k\) 单调不减, 于是可以使用一个指针维护 \(k\)
    第二种: \(\texttt{f[i][j]=}\begin{cases}\texttt{f[i-1][k]-k*c +j*c+(j-h[i])}^2\\\texttt{f[i-1][k]+k*c -j*c+(j-h[i])}^2\end{cases}\)
    可以顺着枚举和倒着枚举来更新:用两个变量维护前若干和后若干的 f[i-1][k]±k*c 的最小值
点击查看代码一
#include <math.h> // 朴素dp:f[i][j]表示将i的高度变为j的最小花费
#include <stdio.h> // f[i][j]=f[i-1][k]+|j-k|*c+(j-h[i])**2
#include <string.h> // j单调时k单调
#include <algorithm>
const int N = 1e5 + 2;
int n, m, c, h[N], res = 0x3f3f3f3f, f[2][102]; // 滚动数组
int main() {
	scanf("%d%d", &n, &c);
	for(int i = 1; i <= n; i ++) scanf("%d", h + i), m = std::max(m, h[i]);
	for(int i = h[1]; i <= m; i ++) f[1][i] = (i - h[1]) * (i - h[1]);
	for(int i = 2; i <= n; i ++)
		for(int k = h[i], j = h[i-1]; k <= m; k ++) {
			while(j < m && f[(i-1)&1][j] + abs(k - j) * c > f[(i-1)&1][j+1] + abs(k - (j+1)) * c) j ++;
			f[i&1][k] = f[(i-1)&1][j] + abs(k - j) * c + (k - h[i]) * (k - h[i]);
		}
	for(int j = h[n]; j <= m; j ++) res = std::min(res, f[n&1][j]);
	printf("%d\n", res);
	return 0;
}
点击查看代码二
#include <math.h> // 朴素dp:f[i][j]表示将i的高度变为j的最小花费
#include <stdio.h> // f[i][j]=f[i-1][k]+|j-k|*c+(j-h[i])**2
#include <string.h> // 将绝对值拆开, 用变量维护f[i-1][k]+-c*k即可
#include <algorithm> // 正着反着都要循环一遍
const int N = 1e5 + 2;
int n, m, c, h[N], res = 0x3f3f3f3f, f[2][102]; // 滚动数组
int main() {
	scanf("%d%d", &n, &c);
	for(int i = 1; i <= n; i ++) scanf("%d", h + i), m = std::max(m, h[i]);
	memset(f[1], 0x3f, sizeof(f[1]));
	for(int i = h[1]; i <= m; i ++) f[1][i] = (i - h[1]) * (i - h[1]);
	for(int i = 2; i <= n; i ++) {
		memset(f[i&1], 0x3f, sizeof(f[i&1]));
		int minn = 0x3f3f3f3f;
		for(int j = 1; j < h[i]; j ++) minn = std::min(minn, f[(i-1)&1][j] - c * j);
		for(int j = h[i]; j <= m; j ++)
			minn = std::min(minn, f[(i-1)&1][j] - c * j),
			f[i&1][j] = std::min(f[i&1][j], minn + c * j + (j - h[i]) * (j - h[i]));
		minn = 0x3f3f3f3f;
		for(int j = m; j >= h[i]; j --)
			minn = std::min(minn, f[(i-1)&1][j] + c * j),
			f[i&1][j] = std::min(f[i&1][j], minn - c * j + (j - h[i]) * (j - h[i]));
	}
	for(int j = h[n]; j <= m; j ++) res = std::min(res, f[n&1][j]);
	printf("%d\n", res);
	return 0;
}
posted @ 2022-10-13 14:56  azzc  阅读(46)  评论(0)    收藏  举报