2023.7.5 NOIP模拟赛

本次集训的第一场模拟赛

大概先把前四题都看了眼 头疼 一道都不会
发现 T2 是上次来 HL 叉哥出的模拟的一道 T6 原题 然而我不会 我谢罪 我反思
还是先看 T1

首先暴力统计 \(c_{i, j}\) 是显然会 T 飞的 所以我们考虑每个点对答案的贡献 未果
然后发现 60pts 的部分分可以暴力统计 \(c_{i, j}\) 想到对每个点暴力用 \(dijkstra\) 跑目标点与起点的通达度 复杂度 \(O(n^2 logm)\) 可过
问题在于松弛操作 首先我们要取 \(dis_u\)\(c_v\) 的最小值 然后和 \(dis_v\) 比 若更大就松弛
当然因为松弛是判更大 所以每次取出来的显然也是 \(dis\) 最大的 重载运算符的时候要注意一下

然后在思考剩下 \(20pts\) 的树的部分分 发现如果将所有点从小到大排序 那么对于那个最小的点 显然它的贡献就是它的点权乘以它连接的几个部分之积
但是如果不是最小点的话 统计的时候就要把它连接的几个部分中已经统计过答案的点断开 不会维护这个 所以溜了

然后是 T2 首先看了下 \(n \le 10\) 的部分分显然白给 打个 \(vis\) 数组再判和 \(i / 2\) 位置的这个数的大小关系即可 先不写
然后是 \(x = y = 1\) 的部分分 考虑将它分为左右两个部分 因为我们只需要考虑相对大小关系 所以 \(n\) 个大小不同的数和 \(1-n\) 是等价的 这样就可以划归成规模更小的子问题 加上组合数就行
顺着这个思路考虑 这题的特殊部分在于钦定的点只有一个 所以每次分成两个小子树只会有一边有限制
然后显然在这个钦定点到根节点的路径上所有数都要小于钦定的这个数 所以每次给它分配不小于它到根节点路径的大于它的数就行 但是我不会维护 所以寄了

但是 T2 还是稳拿的 所以先去看下 T3 T4

那俩比较复杂 但是看了下 T3 的每个数最多出现两次和 T4 链的部分分可能能拿下

先去看的 T4 结果样例没看懂 根本没法推 苦战一会还是无果
T3 不想看了 准备写 T2

首先先把组合数那个玩意写了 写了个看起来很对的记搜 但是没有一个样例是满足这个特殊性质的!!!就离谱
然后手动推了一下 \(n = 1/2/3/4\) 的情况 \(n = 5\) 推不动了(虽然它只有 8 种情况) 但是想了一下因为我还要写爆搜的那 20 分 到时候回来拍一下就行了
然后我就去写爆搜了

\(n \le 10\) 的部分分显然白给 打个 \(vis\) 数组再判和 \(i / 2\) 位置的这个数的大小关系即可 先不写

然后我就把这玩意忘了
然后脑子一短路 写了一个有它转移到两个儿子的爆搜 然后想到可能要枚举排列 还不好判是否搜到头
然后东搞搞西搞搞还是没搞明白 最后断点调试破防了
因为我回溯的搜并且是两个并列结构根本没法搜满

然后不仅这 \(20pts\) 寄了 我没有样例 \(check\) 的那 \(20pts\) 也没法拍了
那就交吧 但愿那 20 分不会挂

然后考试还剩 20 分钟左右 又看了看 T4 是真没看明白啊
然后去看 T3 那个特殊性质 看一会就有思路了
然后就去写 发现是 \(O(n ^ 2)\) 的 假了
但是发现有 60 的 \(O(n ^ 2)\) 的部分分 感觉复杂度很对就转去写那个了
然后又假了 发现这个玩意没了特殊性质是撑死 \(O(n^3 logn)\)
但是 \(10pts\) 的显然够用 但是就剩 \(7\) 分钟了 这玩意细节巨多我根本写不来
然后就坐着开始云 把每个点对都抽象成 \(\left[l, r\right]\) 的一段区间 然后判区间重合
差不多了 然后考试结束了

预计:60 + 20 + 0 + 0
实际:60 + 20 + 0 + 0 rk14

很幸运的是我没拍的 20 分没挂 但实在没啥好高兴的
幸好这不是 NOIP 不然我原地退役得了
然后看了一下大家 T2 有 20 分的基本都是写的爆搜的 20 就我一个是那个特殊性质的 20
中午一交流就想起来最开始那个对的搜法了 难绷


A.道路负载

实际上赛时树的那个思路已经很接近了

个人比较喜欢的一个思路是把点的贡献转化到边上来 即两边点权取 \(min\)
然后因为如果一个边连接两个连通块 它必须是小于任何一个在连通块内部的边 此时它的贡献就是边权 * 连通块 1 的大小 * 连通块 2 的大小
然后因为要求这个边是图里最小的嘛 所以从大到小一个个加边 并查集维护即可

当然直接用点做也是可行的

code:

#include <bits/stdc++.h>
#define ll long long
using namespace std;

const int N = 2e5 + 0721;
int c[N], fa[N], siz[N];
int n, m;
ll ans;

struct node {
	int u, v, w;
	friend bool operator<(node x, node y) {
		return x.w > y.w;
	}
} edge[N];

void init() {
	for (int i = 1; i <= n; ++i) fa[i] = i;
	for (int i = 1; i <= n; ++i) siz[i] = 1;
}

int find(int x) {
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}

int main() {
//	freopen("data2.txt", "r", stdin);
	
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i) scanf("%d", &c[i]);
	for (int i = 1; i <= m; ++i) {
		scanf("%d%d", &edge[i].u, &edge[i].v);
		edge[i].w = min(c[edge[i].u], c[edge[i].v]);
	}
	
	sort(edge + 1, edge + 1 + m);
	init();
	
	for (int i = 1; i <= m; ++i) {
		int fu = find(edge[i].u), fv = find(edge[i].v);
		if (fu == fv) continue;
//		cout << siz[fu] * siz[fv] * edge[i].w << endl;
		ans += 1ll * siz[fu] * siz[fv] * edge[i].w;
		fa[fu] = fv;
		siz[fv] += siz[fu];
		siz[fu] = 0;
	}
	
	printf("%lld", ans);

	return 0;
}

B.小根堆

叉哥精选

同样是基于赛时那个思路
接着考虑 我们设 \(g_{i, j}\) 表示 \(i\) 点填 \(j\) 的方案数
那么就有 \(g_{x, y} = f_x * C^{siz_x - 1}_{n - y}\)
那么答案即为 \(g_{1, 1}\)
我们考虑从 \(x\) 点往上推 最多会推 \(logn\) 个点
考虑 \(x / 2\) 这个点填 \(i\) 的方案数
对于有 \(x\) 在的那个子树 显然它的根节点可以填 \(i + 1 - n\) 的所有数 方案数就是 \(\sum\limits_{j=i + 1}^n g_{x, j}\)
对于另一个子树(即 \(x \operatorname{xor} 1\)) 它自己的方案数是 \(f_{x \operatorname{xor} 1}\) 然后要选出 \(siz_{x \operatorname{xor} 1}\) 个数 从 \(n - i - siz_x\) 个数里面选
这俩乘起来就是 \(g_{x / 2, i} = \sum\limits_{j=i + 1}^n g_{x, j} * f_{x \operatorname{xor} 1} * C ^ {siz_{x \operatorname{xor} 1}} _ {n - i - siz_x}\)
发现前面是个后缀和 \(O(n)\) 预处理一下即可 反正我们还需要 \(O(n)\) 地枚举 \(i\)
那总复杂度就是 \(O(nlogn)\)
有个小优化:因为 \(g_{x/2}\) 只与 \(g_x\) 有关 所以可以用滚动数组压掉一维

code:

#include <bits/stdc++.h>
#define ll long long
using namespace std;

const int mod = 1e9 + 7;
const int N = 1e5 + 0721;
ll fac[N], inv[N], f[N], g[N], h[N];
int siz[N];
int n, loc, num;

void init() {
	fac[0] = fac[1] = 1;
	inv[0] = inv[1] = 1;
	for (int i = 2; i <= n; ++i) {
		fac[i] = fac[i - 1] * i % mod;
		inv[i] = ((mod - mod / i * inv[mod % i]) % mod + mod) % mod;
	}
	for (int i = 2; i <= n; ++i) inv[i] = inv[i - 1] * inv[i] % mod;
}

int C(int x, int y) {
	if (y < x) return 0;
	return fac[y] * inv[x] % mod * inv[y - x] % mod;
}

void dfs(int x) {
	siz[x] = 1;
	int ls = (x << 1), rs = (x << 1 | 1);
	if (ls <= n) {
		dfs(ls);
		siz[x] += siz[ls];
	}
	if (rs <= n) {
		dfs(rs);
		siz[x] += siz[rs];
	}
	return;
}

ll dp(int x) {
	if (f[x] != -1) return f[x];
	if (siz[x] < 3) return 1;
	ll ans = 1;
	int ls = (x << 1), rs = (x << 1 | 1);
	ans = 1ll * dp(ls) * dp(rs) % mod * C(siz[ls], siz[x] - 1) % mod;
	
	return f[x] = ans;
}

void solve() {
	init();
	memset(f, -1, sizeof f);
	dfs(1);
	g[num] = dp(loc) * C(siz[loc] - 1, n - num); 
	while (loc != 1) {
		int tmp = loc;
		loc >>= 1;
		for (int i = n; i >= 1; --i) {
			h[i] = (h[i + 1] + g[i]) % mod;
		}
		for (int i = 1; i <= n; ++i) g[i] = h[i + 1] * dp(tmp ^ 1) % mod * C(siz[tmp ^ 1], n - i - siz[tmp]) % mod;
	}
	printf("%lld",g[1]);
}

int main() {
	
	scanf("%d%d%d", &n, &loc, &num);
	solve();
	
	return 0;
}

C.模式匹配

\(f_i\) 表示到 \(i\) 这位的的最长子串长度
首先有 \(f_i = f_{i - 1}\)
但是显然有可能 \(a_i\) 是 abab 后面那个 b 那 \(f_i\) 就还可以由前面某个位置 \(+4\) 转移过来
显然 \(f_i\) 是单调不减的 所以我们要让第一个 a 尽可能晚地出现
我们还是考虑确定一个找另一个 假如我们确定了 \(b\) 它的前驱是 \(lst_b\)
那么问题就变成了在 b 前面查找一个最大的 \(lst_a\) 使 \(lst_a < lst_b\) 并且 \(a > lst_b\)
这个东西就可以用主席树压掉一维
首先我们第 \(i\) 个版本把 \(nxt_i\) 修改为 \(i\) 并维护区间最大值
这样我们查询的时候查询 \(lst_{b - 1}\) 那个版本 这样这个版本里上传后继的数都是 \(< lst_b\)
然后我们查询 \(\left[lst_b + 1, b - 1\right]\) 这个区间的最大值即可
并且注意此题是允许 \(a = b\) 的 所以对于有 \(lst_{lst_{lst_i}}\) 的要特判一下

code:

#include <bits/stdc++.h>
#define mid (l + r >> 1)
using namespace std;

const int N = 2e7 + 0721;
const int M = 5e5 + 0721;
int a[M], lst[M], nxt[M], loc[M], T[M], f[M];
int n;

struct tree {
	int tr[N], ls[N], rs[N];
	int cnt = 0;
	
	int build(int l, int r) {
		int k = ++cnt;
		if (l == r) return k;
		ls[k] = build(l, mid);
		rs[k] = build(mid + 1, r);
		return k;
	}
	
	int modify(int pre, int l, int r, int loc, int x) {
		int k = ++cnt;
		tr[k] = tr[pre], ls[k] = ls[pre], rs[k] = rs[pre];
		tr[k] = max(tr[k], x);
		if (l == r) return k;
		if (loc <= mid) ls[k] = modify(ls[pre], l, mid, loc, x);
		else rs[k] = modify(rs[pre], mid + 1, r, loc, x);
		return k;
	}
	
	int query(int k, int l, int r, int u, int v) {
		if (u <= l && v >= r) {
			return tr[k];
		}
		int ret = 0;
		if (u <= mid) ret = max(ret, query(ls[k], l, mid, u, v));
		if (v > mid) ret = max(ret, query(rs[k], mid + 1, r, u, v));
		return ret;
	}
	
} seg;

void solve() {
	for (int i = 1; i <= n; ++i) {
		lst[i] = loc[a[i]];
		loc[a[i]] = i;
	}
	memset(loc, 0, sizeof loc);
	for (int i = n; i >= 1; --i) {
		nxt[i] = loc[a[i]];
		loc[a[i]] = i;
	}
	
//	for (int i = 1; i <= n; ++i) cout << i << " " << lst[i] << " " << nxt[i] << endl;
	 
	T[0] = seg.build(1, n);
	
	for (int i = 1; i <= n; ++i) {
		if (nxt[i] == 0) T[i] = T[i - 1];
		else T[i] = seg.modify(T[i - 1], 1, n, nxt[i], i);
	}
//	cerr << seg.query(T[n], 1, n, 1, n) << "\n"; 
	
	for (int i = 1; i <= n; ++i) {
		int tmp = lst[lst[lst[i]]];
		if (tmp != 0) f[i] = f[tmp - 1] + 4;
		int x = lst[i];
		if (x != 0) {
			tmp = seg.query(T[x - 1], 1, n, x + 1, i - 1);
//			cout << i << " " << tmp << "\n";
			if (tmp != 0) f[i] = max(f[i], f[tmp - 1] + 4);
		}
		f[i] = max(f[i], f[i - 1]);
	}
	printf("%d",f[n]);
}

int main() {
//	freopen("ex_21.in", "r", stdin);
	
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	solve();
	
	return 0;
}

D.随机游走

像极了我考试做题的状态

首先那个柿子是 \(f_i = \frac{1}{deg(i)} * \sum\limits (f_j + w_{i, j})\)
其中 \(f_n = 0\)
然后就需要用到高斯消元然后把 \(f_i\) 搞成一个可以 \(O(1)\) 修改的柿子
然后又是什么玩意优化的 因为我不会高斯消元 所以前面的区域以后再来探索吧

posted @ 2023-07-05 19:41  Steven24  阅读(75)  评论(2编辑  收藏  举报