Round 11

前言

好不容易考场上想到5道题的正解,本来以为这次可以考好,但是 T1,T5 挂了,T6 就是调不出来(考完后还调了几个小时)。

T1

很简单的一道题,我们先把 \(0 \sim n - 1\) 每个数计算后存到数组里,然后对于每种相同的数,我们在比它大的数里选 \(m\) 个。

对于选每种数都算出一个答案,最后取最大即可。

至于为什么只有这么点分,因为考试的时候忘记了一个很基本的事情,就是当选定一种数之后,只能让比它大的数通过减少变得跟它一样。

还有一点,就是 map 常数太大了,会被卡掉,明明可以排序的。

code
#include <cstdio>
#include <algorithm>
#include <map>
#define LL long long

using namespace std;

const int N = 3e5 + 5;

LL T, n, m, t, ans, a[N], b[N], la;

int main() {
	scanf("%lld", &T);
	while (T--) {
		scanf("%lld %lld %lld", &n, &m, &t);
		for (LL i = 1; i <= n; i++)
			a[i] = (i - 1) * (i - 1) % m + 1;
		sort(a + 1, a + n + 1);
		ans = 0, a[n + 1] = 0, la = 0;
		for (int i = 1; i <= n; i++) 
			if (a[i] != a[i + 1]) {
				ans = max(ans, i - la + min(t, n - i));
				la = i;
			}
		printf("%lld\n", ans);
	}
	return 0;
} 

T2

考试时唯一过了的一道题。

并查集,以前还考过。

对于每个字母,我们记录第一次出现的字符串是哪个。

如果当前字符串枚举到一个出现过的字母,就把当前字符串和那个字母第一次出现的字符串合并。

最后我们查一下哪些的父亲是自己的个数就是答案。

code
#include <cstdio>
#include <iostream>
#include <cstring>

using namespace std;

const int N = 2e5 + 5, M = 30, P = 1e6 + 5;

int fa[N], n, l, ans, f[N];
char s[P];

inline int FindSet(int x) {
	if (x == fa[x])
		return x;
	return fa[x] = FindSet(fa[x]);
}

inline void UnionSet(int x, int y) {
	int xx = FindSet(x), yy = FindSet(y);
	if (xx != yy)
		fa[xx] = yy;
}

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		fa[i] = i;
	for (int i = 1; i <= n; i++) {
		scanf(" %s", s + 1);
		l = strlen(s + 1);
		for (int j = 1; j <= l; j++)
			if (!f[s[j] - 'a'])
				f[s[j] - 'a'] = i;
			else if (f[s[j] - 'a'] != i)
				UnionSet(i, f[s[j] - 'a']);
	}
	for (int i = 1; i <= n; i++)
		ans += i == FindSet(i);
	printf("%d\n", ans);
	return 0;
}

T3

记忆化搜索。

考试时打的搜索剪枝。

先初始化前缀和 '#' 的个数,方便在 dfs 的时候查询。

dfs 的思路就是如果算出当前矩形完全消除的答案。

再将它分成两部分,分别计算完全消除答案,取最小就是消除当前矩形中所有方块的最小答案了。

一个剪枝:通过前缀和查询当前矩形中有多少方块,如果数量为 \(0\),那个可以直接返回 \(0\)了。

至于考试是为什么不打记忆化,我也想打记忆化,但是我的搜索状态没法记忆化。

code
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 55;

int n, p[N][N], f[N][N][N][N];
char ch;

inline int dfs(int x1, int y1, int x2, int y2) {
	if (!(p[x2][y2] + p[x1 - 1][y1 - 1] - p[x2][y1 - 1] - p[x1 - 1][y2]))	//如果当前矩形中没有方块,直接返回。 
		return 0;
	if (f[x1][y1][x2][y2])	//当前矩形已经求过 ,没必要再求一边。 
		return f[x1][y1][x2][y2];
	int res = max(x2 - x1 + 1, y2 - y1 + 1);	//不分成两部分的答案。 
	for (int i = x1; i < x2; i++)	//分别计算横着分和竖着分的答案。 
		res = min(res, dfs(x1, y1, i, y2) + dfs(i + 1, y1, x2, y2));
	for (int i = y1; i < y2; i++)
		res = min(res, dfs(x1, y1, x2, i) + dfs(x1, i + 1, x2, y2));
	return f[x1][y1][x2][y2] = res;
}

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		scanf(" ");
		for (int j = 1; j <= n; j++) {
			ch = getchar();
			p[i][j] = p[i - 1][j] + p[i][j - 1] - p[i - 1][j - 1] + (ch == '#'); //前缀和。 
		}
	}
	printf("%d\n", dfs(1, 1, n, n));
	return 0;
}

T4

概率dp。

暴力的思路是枚举哪一条边不选,算出答案,求出所有答案的最小值。

但如果我们枚举的是点,再在以这个点为边的起点的边中选一条边不选,那么时间复杂度就会降到 \(O(nm)\),足以通过此题。

我们先来考虑不删怎么算答案,根据期望的线性性,我们可以从 \(n\) 倒推。

对于当前点 \(i\) 的期望,我们枚举与它连边的点 \(j\),那么 \(j\) 贡献给 \(i\) 的期望为 \(\dfrac{1}{s} \times f_j\)\(f_j\) 是从 \(n\)\(j\) 的期望边数,\(s\)\(i\) 的出度,也就是以 \(i\) 为起点的边的数量)。

那么我们现在要断掉以 \(i\) 为起点的一条边,根据贪心,我们要让期望边数最小,那么我们肯定就要从枚举到的 \(j\) 里期望最大的删掉。

还有一个很重要的点每条边的起点一定小于终点,所以不会出现环。

注意:如果一个点出边只有一条,那么就不能删掉,因为我们要保证每个点都可以到 \(n\)

code
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 605;

int n, m;
vector<int> ve[N];
double f[N], ans = 1e9;

int main() {
	scanf("%d %d", &n, &m);
	for (int i = 1, u, v; i <= m; i++) {
		scanf("%d %d", &u, &v);
		ve[u].push_back(v);
	}
	for (int i = 1; i < n; i++) { //枚举要断掉的边的起点 
		for (int j = 1; j <= n; j++)	//每次都算一个答案 
			f[j] = 0;
		for (int j = n - 1; j; j--) {	//倒推算出每个点的期望,可以倒推的原因是保证了u<v 
			double s = 0;
			for (auto k : ve[j])	//算当前点期望 
				f[j] += f[k], s = max(s, f[k]);
			if (i == j && ve[j].size() > 1)	//如果当前点是我们要断边的那个点并且出度大于1说明可以选一条最长的断掉 
				f[j] -= s, f[j] /= (ve[j].size() - 1);
			else
				f[j] /= ve[j].size();  //否则不断 
			f[j]++;
		}
		ans = min(ans, f[1]);
	}
	printf("%.6lf\n", ans);
	return 0;
}

T5

考试时没算 \(f_0 = 1\),导致从 \(n = 6\) 开始就错了,没发现错的原因是暴力一起打错了。

这道题其实很简单,我们 dp 时要保证不算重,只需要保证后几位不同,前面可以随便选。

大概是这样:

这样是选后四个。

选后两个:

很明显发现他们不一样,那么在这两种情况下,前面的都可以随便选,因为保证了最后面不同。

画一下发现后 \(2\) 个有 \(3\) 种,其他都是 \(2\) 种。

这是后两个的:


这是后四个的:

后四个以上的就在后四个的基础上在中间加横着的即可,所以后六个就是这样:

这个时候状态转移方程也就出来了,\(i \in [1, \dfrac{n}{2}]\)(因为为了方便我把 \(n\) 除以 \(2\) 了):

\[f_i = 3f_{i - 1} + \sum_{j = 0}^{i - 2}2f_{j} \]

注意:\(f_0 = 1\)(100 分就这样没了)

这样复杂度是 \(O(n)\) 的,但是我们可以把系数 \(2\) 提出来,得到:\(f_i = 3f_{i - 1} + 2\sum_{j = 0}^{i - 2}f_{j}\)

所以我们维护两个,一个数是前缀和 \(s\),一个数是 \(f_{i - 1}\)

很明显更新 \(f_i\)\(f_i = 3 \times f_{i - 1} + 2 \times s\)\(sum = 1 \times f_{i - 1} + 1 \times sum\)

用乘号的原因是矩阵加速已经呼之欲出了。

初始矩阵:

\[\left[ \begin{matrix} f_{2} & sum \\ \end{matrix} \right] \]

\(sum\) 其实就是 \(f_0 + f_1\)(别忘了 \(f_0\),重要的事情说三遍)

加速矩阵:

\[\left[ \begin{matrix} 3 & 1 \\ 2 & 1 \\ \end{matrix} \right] \]

用初始矩阵乘上加速矩阵的 \(n - 2\) 次方后矩阵的第一项就是答案。

code
#include <cstdio>
#include <cstring>
#define LL long long

const int N = 1e6 + 5;
const LL Mod = 1e9 + 7;

struct Matrix {
    LL a[5][5];
    inline Matrix() { memset(a, 0, sizeof(a)); }
    inline Matrix operator*(Matrix b) { //矩阵乘法,因为只有2*2,所以没写循环 
        Matrix c;
        c.a[1][1] = (a[1][1] * b.a[1][1] % Mod + a[1][2] * b.a[2][1] % Mod) % Mod;
        c.a[1][2] = (a[1][1] * b.a[1][2] % Mod + a[1][2] * b.a[2][2] % Mod) % Mod;
        c.a[2][1] = (a[2][1] * b.a[1][1] % Mod + a[2][2] * b.a[2][1] % Mod) % Mod;
        c.a[2][2] = (a[2][1] * b.a[1][2] % Mod + a[2][2] * b.a[2][2] % Mod) % Mod;
        return c;
    }
} ans, s;

inline void qkpow(LL x) {	//快速幂 
    while (x) {
        if (x & 1)
            ans = ans * s;
        s = s * s;
        x >>= 1;
    }
}

LL n;

int main() {
    scanf("%lld", &n);
    n /= 2;
    if (n == 1)
        return puts("3"), 0;
    if (n == 2)
        return puts("11"), 0;
    ans.a[1][1] = 11, ans.a[1][2] = 4;	//初始矩阵 
    s.a[1][1] = 3, s.a[1][2] = 1;	//加速矩阵 
    s.a[2][1] = 2, s.a[2][2] = 1;
    qkpow(n - 2);
    printf("%lld\n", ans.a[1][1]);
    return 0;
}

T6

用平衡树维护数列。

对于操作 \(2\) 直接用两个 split 将 \(x\) 裂出来输出 \(key\) 即可。

对于操作 \(1\),我们肯定要先找一个 \(1 \sim x - 1\) 中下标最大的但是当前高度小于 \(y\) 的,记为 \(a\)

注意:\(a\) 是从平衡树中分裂出来的点的下标,并不是真实在数列中的下标,\(a\) 在数列中的下标是 \(a\) 在平衡树中的排名,而 \(x\) 确实在数列中的真实下标

很明显发现 \(a + 1 \sim x\) 会被向左移一格,所以移动数量就是 \(\sum_{i = a + 1}^{x}(h_i - y)\),将 \(y\) 提出来 \(\sum_{i = a + 1}^{x}h_i - (x - a)y\),所以对于求移动个数,在 FHQ 种维护一个 \(sum\) 然后将 \(a + 1 \sim x\) 裂出来就可以算出答案。

更重要的点在于如何对数列进行修改达到题目要求效果。

因为 \(a + 1 \sim x\) 会被向左移一格,所以 \(a + 2\)\(x\) 其实没变,只是向左平移了,而 \(a + 1\) 高于 \(y - 1\) 的部分则是给了 \(a\),给完后移到 \(x\) 后,可以想一下为什么。

现在有一个问题就是如何找到 \(a\)

我们可以维护一个 \(mi\),表示在当前点为根的子树中,最小高度是多少,然后写一个 find 函数。

我们先把 \(1 \sim x - 1\) split 出来,然后我们去这颗分裂出来的树中找,对于当前节点 \(rt\),如果右子树有小于 \(y\) 的就递归右子树,否则如果 \(rt\)\(key\) 小于 \(y\) 就返回 \(rt\),如果还是不行,就递归左子树,如果 \(rt = 0\),就返回 \(0\),因为没找到小于 \(y\) 的。

因为我们保证了先找右子树,再找根,最后找左子树,所以只要找到答案一定就是离 \(x\) 最近的。

注:如果操作 \(1\)\(y\) 大于 \(h_{x}\) 那么这次操作也不能进行。

code
#include <cstdio>
#include <cstdlib>
#include <utility>
#include <ctime>
#include <algorithm>
#define LL long long
#define int long long

using namespace std;

const int N = 2e5 + 5;

struct Treap {
	int tot, root;
	struct node {
		int l, r, rank, siz, f;
		LL key, sum, mi;
	} a[N];
	inline int New(int val) {
		a[++tot].key = val, a[tot].rank = rand();
		a[tot].siz = 1, a[tot].mi = val, a[tot].sum = val;
		a[tot].l = a[tot].r = a[tot].f = 0, a[a[tot].l].mi = a[a[tot].r].mi = 1e15;	//初始化。 
		return tot;
	}
	inline void push_up(int rt) {
		a[rt].siz = a[a[rt].l].siz + a[a[rt].r].siz + 1;
		a[rt].mi = 1e15;	//更新以当前点为根的子树下的最小值,方便find函数使用。 
		if (a[rt].key)
			a[rt].mi = a[rt].key;
		if (a[rt].l)
			a[rt].mi = min(a[rt].mi, a[a[rt].l].mi);
		if (a[rt].r)
			a[rt].mi = min(a[rt].mi, a[a[rt].r].mi);
		if (!a[a[rt].l].mi)
			a[a[rt].l].mi = 1e15;
		if (!a[a[rt].r].mi)
			a[a[rt].r].mi = 1e15;
		a[a[rt].l].f = a[a[rt].r].f = rt;	//求出每个点的父节点,方便查询排名。 
		a[rt].sum = a[a[rt].l].sum + a[a[rt].r].sum + a[rt].key;	//更新更新以当前点为根的子树的高度和,方便查询移动个数。 
	}
	inline int merge(int rt1, int rt2) {
		if (!rt1)
			return rt2;
		if (!rt2)
			return rt1;
		if (a[rt1].rank < a[rt2].rank) {
			a[rt1].r = merge(a[rt1].r, rt2);
			push_up(rt1);
			return rt1;
		} else {
			a[rt2].l = merge(rt1, a[rt2].l);
			push_up(rt2);
			return rt2;
		}
	}
	inline pair<int, int> split(int rt, int k) {
		if (!rt)
			return make_pair(0, 0);
		if (a[a[rt].l].siz + 1 <= k) {
			pair<int, int> ans = split(a[rt].r, k - a[a[rt].l].siz - 1);
			a[rt].r = ans.first;
			push_up(rt);
			return make_pair(rt, ans.second);
		} else {
			pair<int, int> ans = split(a[rt].l, k);
			a[rt].l = ans.second;
			push_up(rt);
			return make_pair(ans.first, rt);
		}
	}
	inline int find(int rt, LL k) {
		if (!rt)
			return 0;
		if (a[a[rt].r].mi < k)
			return find(a[rt].r, k);
		if (a[rt].key < k)
			return rt;
		return find(a[rt].l, k);
	}
	inline int get_rank(int rt) {
		int res = 0;
		for (int i = rt; i; i = a[i].f) 
			if (a[a[i].f].r == i)
				res += a[a[a[i].f].l].siz + 1;
		return res + a[a[rt].l].siz + 1;
	}
} BST;
int n, q, opt, x, y, T;

signed main() {
	srand(time(0));
	scanf("%lld", &T);
	while (T--) {
		scanf("%lld %lld", &n, &q);
		BST.tot = BST.root = 0;
		for (int i = 1; i <= n; i++) {
			scanf("%d", &x);
			BST.root = BST.merge(BST.root, BST.New(x));
		}
		while (q--) {
			scanf("%lld", &opt);
			if (opt == 1) {
				scanf("%lld %lld", &x, &y);
				pair<int, int> ans = BST.split(BST.root, x - 1);
				int a = BST.find(ans.first, y);	//找到离x最近的高度又小于y的点的编号。 
				int rank = BST.get_rank(a);	//求出在序列中真实下标。 
				BST.root = BST.merge(ans.first, ans.second);	//分裂后要合并回去。 
				pair<int, int> A1 = BST.split(BST.root, x);	//将x这个点分裂出来。 
				pair<int, int> A2 = BST.split(A1.first, x - 1);
				if (!a || BST.a[A2.second].key < y) {	//如果没有高度比y小的或y大于x的当前高度当前操作就 不能进行。 
					puts("0");
					BST.root = BST.merge(BST.merge(A2.first, A2.second), A1.second);
				} else {
					BST.root = BST.merge(BST.merge(A2.first, A2.second), A1.second);	//将a+1~x裂出来求高度和。 
					pair<int, int> a1 = BST.split(BST.root, x);
					pair<int, int> a2 = BST.split(a1.first, rank);
					printf("%lld\n", BST.a[a2.second].sum  - (y - 1) * (x - rank));
					BST.root = BST.merge(BST.merge(a2.first, a2.second), a1.second);	//合并回去。
					if (rank == x - 1) {	//代码是根据考场恶臭代码调的,其实这个if没必要,只需要下面的else的内容就可以了。 
						pair<int, int> ans1 = BST.split(BST.root, x);
						pair<int, int> ans2 = BST.split(ans1.first, x - 1);
						pair<int, int> ans3 = BST.split(ans2.first, rank);
						pair<int, int> ans4 = BST.split(ans3.first, rank - 1);
						int d = BST.a[ans2.second].key - y + 1;
						BST.a[ans4.second].key += d, BST.a[ans2.second].key -= d;
						BST.a[ans4.second].sum += d, BST.a[ans2.second].sum -= d;
						BST.a[ans4.second].mi += d, BST.a[ans2.second].mi -= d;
						BST.root = BST.merge(BST.merge(BST.merge(ans4.first, ans4.second), ans3.second), BST.merge(ans2.second, ans1.second));
					} else {
						pair<int, int> ans1 = BST.split(BST.root, x);
						pair<int, int> ans2 = BST.split(ans1.first, rank - 1);
						pair<int, int> ans3 = BST.split(ans2.second, 2);
						pair<int, int> ans4 = BST.split(ans3.first, 1);	//ans1.second是 x+1~n,ans2.first是 1~a-1,ans3.second是a+2~x,ans4.first是a,ans4.second是a+1。 
						int d = BST.a[ans4.second].key - y + 1;	//a+1高出y-1的部分就是a加上的部分。 
						BST.a[ans4.second].key -= d, BST.a[ans4.first].key += d;
						BST.a[ans4.second].sum -= d, BST.a[ans4.first].sum += d;
						BST.a[ans4.second].mi -= d, BST.a[ans4.first].mi += d;
						BST.root = BST.merge(ans2.first, BST.merge(ans4.first, BST.merge(ans3.second, BST.merge(ans4.second, ans1.second)))); //合并时将a+1移到x后。 
					}
				}
			} else {
				scanf("%lld", &x);
				pair<int, int> ans1 = BST.split(BST.root, x);
				pair<int, int> ans2 = BST.split(ans1.first, x - 1);
				printf("%lld\n", BST.a[ans2.second].key);
				BST.root = BST.merge(BST.merge(ans2.first, ans2.second), ans1.second);
			}
		}
		for (int i = 1; i <= n; i++) {
			pair<int, int> ans1 = BST.split(BST.root, i);
			pair<int, int> ans2 = BST.split(ans1.first, i - 1);
			printf("%lld ", BST.a[ans2.second].key);
			BST.root = BST.merge(BST.merge(ans2.first, ans2.second), ans1.second);
		}
		puts("");
	}
	return 0; 
}

总结

要理性分配时间,知道 T6 很有可能调不出来应该先打暴力再去检查前面的题,不应该死磕一道题。

posted @ 2023-08-25 14:45  zhy。  阅读(13)  评论(0编辑  收藏  举报