8.23 模拟赛小记

A.认真读题很重要

还是单调队列优化 dp 的板子,类似昨天 C。


B.打暴力也能得分

洛谷原题指路:P1758 [NOI2009] 管道取珠

感觉是比较有难度的 dp。

题目概述:给你两个只有两种字符组成的序列,每次从一个序列末尾取走一位放入新序列的末尾,最后得到 k 种不同的新序列形态。每种形态有 a[i] 种不同的操作,求 \(\sum_{i = 1}^{n}{a[i]^2}\)

首先想怎么转换 \({a[i]^2}\)。老师提到了P1004 [NOIP2000 提高组] 方格取数P1006 [NOIP2008 提高组] 传纸条
。这两个题目的扩展就有方格取数取 2 次、3 次、k 次等等。所以如果了解过的话,容易想到将平方转换为两次取到了相同序列的方案数。

设 f[i][j][x][y] 表示取了第一次取第一个序列 i 个、第二个序列 j 个。第二次取第一个序列 x 个、第二个序列 y 个的答案平方和。

因为 i + j = x + y,所以可以直接缩掉一维。

考虑取 a[i + 1]、a[j + 1]、b[x + 1]、b[y + 1]。那么就四种情况:11、12、21、22(大概懂我的意思吧?),然后兑起来进行转移。

但是数组如果用 N^3 显然会炸,且 f[i][j][x] 中影响到 i 的只有 i - 1,所以我们用滚动数组滚掉一维。用 cnt 记当前 i 的奇偶,cnt^1 就可以表示前一位的 i - 1。

#include<bits/stdc++.h>
using namespace std;
const int N = 510;
const int mod = 1024523;
int n, m;
char a[N], b[N];
int f[2][N][N];
int main()
{
	scanf("%d%d%s%s", &n, &m, a + 1, b + 1);
	f[0][0][0] = 1;
	int cnt = 0;
	for(int i = 0; i <= n; i ++, cnt ^= 1)
	{
		for(int j = 0; j <= m; j ++ )
		{
			for(int x = 0; x <= n; x ++ )
			{
				int y = i + j - x;
				int t = f[cnt][j][x];
				if(! t || y < 0 || y > m) continue;
				if(a[i + 1] == a[x + 1]) f[cnt^1][j][x + 1] = (f[cnt^1][j][x + 1] + t) % mod;
                if(a[i + 1] == b[y + 1]) f[cnt^1][j][x] = (t + f[cnt^1][j][x]) % mod;
                if(b[j + 1] == a[x + 1]) f[cnt][j+1][x+1] = (t + f[cnt][j + 1][x + 1]) % mod;
                if(b[j + 1] == b[y + 1]) f[cnt][j+1][x] = (t + f[cnt][j + 1][x]) % mod;
                f[cnt][j][x] = 0;
			}
		}
	}
	printf("%d", f[cnt][m][n]); 
}

太妙了,感觉真的充分发挥人类的智慧捏。


C.提交前删掉调试

洛谷原题指路:P2885 [USACO07NOV] Telephone Wire G

题目:给出若干棵树的高度,可以进行一种操作:把某棵树增高h,花费为 h * h。操作完成后连线,两棵树间花费为高度差 * 定值 c。求两种花费加和最小值。

赛时认为本题很难,赛后发现真的水。

用 f[i][j] 表示选到了第 i 个树时这棵树高度为 j 的最小花费。

一个暴力 dp 很好想:

\(f[i][j] = min(f[i - 1][k] + c * abs(k - j)) + (h[i] - j)^2\)

然后时间复杂度是 \(O(nh^2)\) 的。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 2;
const int inf = 0x7fffffff;
int n, m;
int a[N];
int f[N][102];
int main()
{
	scanf("%d%d", &n, &m);
	int maxn = 0;
	for(int i = 1; i <= n; i ++ )
	{
		scanf("%d", &a[i]);
		maxn = max(maxn, a[i]);
	}
	for(int i = 1; i <= n; i ++ )
	{
		for(int j = a[i]; j <= maxn; j ++ )
		{
			int t = inf;
			for(int k = a[i - 1]; k <= maxn; k ++ )
			{
				t = min(t, f[i - 1][k] + abs(k - j) * m);
			}
			f[i][j] = (a[i] - j) * (a[i] - j) + t;
		}
	}
	int ans = inf;
	for(int i = a[n]; i <= maxn; i ++ ) ans = min(ans, f[n][i]);
	printf("%d", ans);
}

无奈本题数据太弱,或者是洛谷评测机跑的太快,1e9 竟然过了,而且跑得飞快。但这个复杂度显然不对,我们需要优化一下。

观看上面的式子,思考如何优化求 \(min(f[i - 1][k] + c * abs(k - j))\) 的时间。

为了化简本式,我们分类讨论 k 和 j 的大小关系,然后把式子拆开,动态的求 min。

让枚举 k 的一部分合并到枚举 j 的过程中,这样当 k < j 时我们正序枚举 j 的情况,这样始终能保证 k <= j。反之倒序枚举,同理。

int t = inf;
for(int k = 1; k <= a[i]; k ++ ) t = min(t, f[i - 1][k] - m * k);
for(int j = a[i]; j <= maxn; j ++ )
{
	t = min(t, f[i - 1][j] - m * j);
	f[i][j] = min(f[i][j], t + m * j + (j - a[i]) * (j - a[i]));
}
t = inf;
for(int j = maxn; j >= a[i]; j -- )
{
	t = min(t, f[i - 1][j] + m * j);
	f[i][j] = min(f[i][j], t - m * j + (j - a[i]) * (j - a[i]));
}

把第一个树初始化,以及 f[0][0][0] = 1。

最后的复杂度为 \(O(nh)\)

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 2;
const int inf = 0x7fffffff;
int n, m;
int a[N];
int f[N][102];
int main()
{
	scanf("%d%d", &n, &m);
	int maxn = 0;
	for(int i = 1; i <= n; i ++ )
	{
		scanf("%d", &a[i]);
		maxn = max(maxn, a[i]);
	}
	memset(f, 0x3f, sizeof f);
	for(int i = a[1]; i <= maxn; i ++ ) f[1][i] = (a[1] - i) * (a[1] - i);
	for(int i = 2; i <= n; i ++ )
	{
		int t = inf;
		for(int k = 1; k <= a[i]; k ++ ) t = min(t, f[i - 1][k] - m * k);
		for(int j = a[i]; j <= maxn; j ++ )
		{
			t = min(t, f[i - 1][j] - m * j);
			f[i][j] = min(f[i][j], t + m * j + (j - a[i]) * (j - a[i]));
		}
		t = inf;
		for(int j = maxn; j >= a[i]; j -- )
		{
			t = min(t, f[i - 1][j] + m * j);
			f[i][j] = min(f[i][j], t - m * j + (j - a[i]) * (j - a[i]));
		}
	}
	int ans = inf;
	for(int i = a[n]; i <= maxn; i ++ ) ans = min(ans, f[n][i]);
	printf("%d", ans);
}

D.贪心只能过样例

据说老师本题是出错了,临时改的数据。但不管怎么说,这题又到我的知识漏洞了。

题目概述:有两列长度为 n 的数字,从第 1 行走到第 n 行,每一行只能走二者之一的格子。将你得到的每一个格子数的平方求和后有多少不同的答案。

思路来自同机房大佬,自己想不出来。stO。

大概用了状压 dp 的思想吧,用 f[i][j] 表示走到第 i 行时 j 这个数字能否被拼出来,然后每次枚举 j,再分别考虑这两个格子的取值是否能拼,若能则将答案转移。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const long long N = 104;
int n,a[N][2];
bool f[N][N * N * N];
ll ans;
int main()
{
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) scanf("%d%d", &a[i][1], &a[i][2]);
	f[1][a[1][1] * a[1][1]] = f[1][a[1][2] * a[1][2]] = 1;
	for(int i = 2; i <= n; i ++ )
	{
		for(int j = 1; j <= i * N * N; j ++ )
		{
			if (j - a[i][1] * a[i][1] > 0) f[i][j] |= f[i - 1][j - a[i][1] * a[i][1]];
			if (j - a[i][2] * a[i][2] > 0) f[i][j] |= f[i - 1][j - a[i][2] * a[i][2]];
		}
	}
	for(int i = 1; i <= n * N * N; i ++ ) if(f[n][i]) ans ++;
	printf("%lld\n", ans);
	return 0;
}

同机房大佬还写的 bitset。这种黑科技目前对我来说还是过于高级了点,不是很会用啊所以就不说怎么写的了。


赛后好生气啊,怎么赛时什么也想出不出来呢,也不算难啊(x

posted @ 2023-08-23 23:00  Moyyer_suiy  阅读(20)  评论(1编辑  收藏  举报