AtCoder Beginner Contest 253[题解D~Ex]

\(ABC253\)

\(D\)

\(Problem\)

找出 \(1\)\(N\) 中有多少个数既不是 \(A\) 的倍数也不是 \(B\) 的倍数,求它们的和是多少。

\(1\leq N,A,B\leq 10^9\)

\(Sol\)

简单容斥。

\[ans = \frac{n\times (n-1)}{2}- \sum A_i - \sum B_i + \sum C_i \]

\(A_i\) 是范围内 \(A\) 的倍数,\(B_i\) 同理,\(C_i\) 是范围内即是 \(A\) 的倍数又是 \(B\) 的倍数的数。

\(code\)

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e2 + 10;
inline int read()
{
	int s = 0, w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
	return s * w;
}
int n, A, B, ans;
inline int gcd(int a, int b) { return !b ? a : gcd(b, a % b); }
inline int calc(int x, int y, int c) { return (x + y) * c / 2; }
signed main()
{
	n = read(), A = read(), B = read();
	int g = gcd(A, B);
	int mi = A * B / g;
	ans = calc(1, n, n);
	ans = ans - calc(A, (n / A) * A, (n / A));
	ans = ans - calc(B, (n / B) * B, (n / B));
	ans = ans + calc(mi, (n / mi) * mi, (n / mi));
	printf("%lld\n", ans);
	return 0;
}

\(E\)

\(Problem\)

对于一个长为 \(N\) 的序列 \(A\),称其合法当且仅当满足一下条件:

  • \(1\leq A_i\leq M\)

  • \(|A_i - A_{i+1}|\ge K\) \((1\leq i\leq N - 1)\)

给定 \(N\),\(M\),\(K\),求合法序列的个数。

\(2\leq N\leq 1000,1\leq M\leq 5000,0\leq K\leq M - 1\)

\(Sol\)

简单 \(dp\),设 \(dp[i][j]\) 表示前 \(i\) 个数满足合法且第 \(i\) 个数为 \(j\) 的序列数量。

显然,每次更新我们要枚举当前填的数和上一个数填的数,又注意到上一个数填的数是两端连续的区间,前缀和优化可以省去枚举。

只需注意 \(K = 0\) 的情况,计算的时候两端区间可能会交在一起。

\(code\)

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e3 + 10, M = 5e3 + 10, mod = 998244353;
inline int read()
{
	int s = 0, w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
	return s * w;
}
int n, m, k;
int dp[N][M], sum[M];
signed main()
{
	n = read(), m = read(), k = read();
	for(register int i = 1; i <= m; i++) dp[1][i] = 1;
	for(register int i = 2; i <= n; i++){
		//cout << "Begin: " << i << "\n";
		for(register int j = 1; j <= m; j++)
			sum[j] = (sum[j - 1] + dp[i - 1][j]) % mod;
		for(register int j = 1; j <= m; j++){
			if(j - k >= 1) dp[i][j] = (dp[i][j] + sum[j - k]) % mod;
			if(j + k <= m){
				int to = j + k - 1;
				if(to < j - k) to = j - k;
				dp[i][j] = (dp[i][j] + (sum[m] - sum[to]) % mod + mod) % mod; 
			}
		}
	}
	int ans = 0;
	for(register int i = 1; i <= m; i++) ans = (ans + dp[n][i]) % mod;
	printf("%lld\n", ans);
	return 0;
}

\(F\)

\(Problem\)

给定一个 \(N\times M\) 的矩阵,最开始矩阵里的值都为 \(0\),接下来需要完成 \(Q\) 个操作,每一个操作可能如下:

  • \(1,l,r,x\),对矩阵第 \(l\)\(r\) 列的数全部加上 \(x\)

  • \(2,i,x\),将第 \(i\) 行的元素重置为 \(x\)

  • \(3,i,j\),输出第 \(i\) 行第 \(j\) 列的元素大小。

\(1\leq N,M,Q\leq 2\times 10 ^ 5\)

\(Sol\)

很容易想到线段树维护列,但由于行的重置操作,需要我们查找不同时间节点线段树的值。

主席树显然可以做到,只不过要区间修改,注意空间调大即可。

同样,这题也可以不用主席树维护。发现对于每一个 \(3\) 询问,我们只需要知道其对应的行与最近一次重置是什么时候,并在那个时候记录下记录下线段树对应列的值,一样可以达到目的。由于询问次数有限制,每个点在这种情况下只被记录一次,所以时间复杂度可行,且只需要一棵线段树维护。

\(code\)

主席树做法

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e7 + 10, M = 1e6 + 10;
inline int read()
{
	int s = 0, w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
	return s * w;
}
int n, m, Q, now;
int arr[N], brr[N];
int a[M], ls[N], rs[N], lazy[N], idx;
int root[M], sum[N];
inline void pushup(int k, int l, int r) { sum[k] = sum[ls[k]] + sum[rs[k]] + (r - l + 1) * lazy[k]; }
inline void build(int k,int l,int r)
{
	lazy[k] = 0, sum[k] = 0;
	if(l == r) { sum[k] = a[l]; return; }
	int mid = (l + r) >> 1;
	ls[k] = ++idx, rs[k] = ++idx;
	build(ls[k], l, mid), build(rs[k], mid + 1, r);
	pushup(k, l, r);
}
inline void update(int pre, int k, int x, int y, int val, int l, int r)
{
	ls[k] = ls[pre];
	rs[k] = rs[pre], sum[k] = sum[pre], lazy[k] = lazy[pre];
	if(l >= x && r <= y){
		lazy[k] += val;
		sum[k] += (r - l + 1) * val;
		return ;
	}
	int mid = (l + r) >> 1;
	if(x <= mid) ls[k] = ++idx, update(ls[pre], ls[k], x, y, val, l, mid);
	if(mid + 1 <= y) rs[k] = ++idx, update(rs[pre], rs[k], x, y, val, mid + 1, r);
	pushup(k, l, r);
}
inline int query(int k, int x, int y, int lz, int l, int r)
{ 
	if(l >= x && r <= y) return lz * (r - l + 1) + sum[k];
	int mid = (l + r) >> 1;
	int ans = 0;
	if(x <= mid) ans += query(ls[k], x, y, lz + lazy[k], l, mid);
	if(mid + 1 <= y) ans += query(rs[k], x, y, lz + lazy[k], mid + 1, r);
	return ans;
}
signed main()
{
	n = read(), m = read(), Q = read();
	root[0]=++idx;
	build(root[0],1,m);
	for(register int i = 1; i <= n; i++) brr[i] = root[0];
	while(Q--){
		int opt = read();
		if(opt == 1){
			int l = read(), r = read(), x = read();
			root[++now] = ++idx;
			update(root[now - 1], root[now], l, r, x, 1, m);
		}
		if(opt == 2){
			int i = read(), x = read();
			arr[i] = x, brr[i] = root[now];
		}
		if(opt == 3){
			int i = read(), j = read();
			int mx = query(root[now], 1, j, 0, 1, m) - (j > 1 ? query(root[now],1,j - 1,0,1,m) : 0);
			//cout << "yes\n";
			int mi = query(brr[i], 1, j, 0, 1, m) - (j > 1 ? query(brr[i], 1, j - 1, 0, 1, m) : 0);
			printf("%lld\n", mx - mi + arr[i]);
		}
	}
	return 0;
}

线段树做法

#include <bits/stdc++.h>
#define int long long
#define mp make_pair
using namespace std;
const int N = 2e5 + 10;
inline int read()
{
	int s = 0, w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
	return s * w;
}
struct node{
	int opt, l, r, x;
}que[N];
int n, m, Q;
int pre[N], arr[N];
map<pair<int, int>, int> vl;
int tag[4 * N];
vector<pair<int, int> > vec[N];
inline void pushdown(int k)
{
	tag[k << 1] += tag[k], tag[k << 1 | 1] += tag[k], tag[k] = 0;
}
inline void change(int k, int l, int r, int x, int y, int v)
{
	if(r < x || l > y) return;
	if(l >= x && r <= y) { tag[k] += v; return; }
	pushdown(k);
	int mid = (l + r) >> 1;
	change(k << 1, l, mid, x, y, v), change(k << 1 | 1, mid + 1, r, x, y, v);
}
inline int query(int k, int l, int r, int x)
{
	//cout << l << " " << r << " " << x << "\n";
	if(l == r && l == x) return tag[k];
	pushdown(k);
	int mid = (l + r) >> 1;
	if(x <= mid) return query(k << 1, l, mid, x);
	else return query(k << 1 | 1, mid + 1, r, x);
}
signed main()
{
	n = read(), m = read(), Q = read();
	for(register int i = 1; i <= Q; i++){
		que[i].opt = read(), que[i].l = read(), que[i].r = read();
		if(que[i].opt == 1) que[i].x = read();
		if(que[i].opt == 2) pre[que[i].l] = i; //设置第 i 行新的更新位置 
		if(que[i].opt == 3)
			if(pre[que[i].l]) vec[pre[que[i].l]].push_back(mp(que[i].l, que[i].r));
	}
	for(register int i = 1; i <= Q; i++){
		if(que[i].opt == 1) change(1, 1, m, que[i].l, que[i].r, que[i].x);
		if(que[i].opt == 2){
			arr[que[i].l] = que[i].r;
			for(register int j = 0; j < vec[i].size(); j++)
				vl[vec[i][j]] = query(1, 1, m, vec[i][j].second);
		}
		if(que[i].opt == 3){	
			pair<int, int> x = mp(que[i].l, que[i].r);
		//	cout << "yes\n";
			if(!vl[x]) printf("%lld\n", query(1, 1, m, que[i].r) + arr[que[i].l]);
			else printf("%lld\n", query(1, 1, m, que[i].r) + arr[que[i].l] - vl[x]);
		}
	}
	return 0;
}

\(G\)

\(Problem\)

对于任意 \(N\) 大于等于 \(2\),有 \(\frac{N(N-1)}{2}\) 个二元组 \((x,y)\) 满足 \(1\leq x < y\leq N\)

将这些二元组以 \(x\) 为第一关键字按照字典序排列。

对于长度为 \(N\) 的序列 \(A\) 满足 \(A_i = i\),给定区间 \(L\)\(R\),使之按照排名 \(L\)\(R\) 的二元组一次进行如下操作:

  • \(swap(A_x,A_y)\)

输出操作后的 \(A\) 数组。

\(2\leq N\leq 2\times 10^5,1\leq L\leq R\leq \frac{N(N-1)}{2}\)

\(Sol\)

比较简单的一道找规律题。

按照 \(x\),分成不同类型的操作分别看待。

对于 \(x\)\(1\)\(n-1\),操作的数量从 \(n-1\)\(1\)。对于每一种类型的操作,如果全部执行,效果等价于将当前的 \(A_n\) 移动到 \(A_x\),将原来的 \(A_x\)\(A_{n-1}\) 向后移动一位。

显然的是,在区间内,我们只会碰到两种类型没有完全被操作,一次在开头,一次在结尾。

对于这两次操作我们找到区间暴力交换。

对于所有中间的操作,我们记录第一个被操作完的区间的位置,记录被操作完的区间的数量,则所有的操作简化为将开头的一段区间移动到末尾。

注意一些细节即可。

\(code\)

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10;
inline int read()
{
	int s = 0, w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
	return s * w;
}
int n, l, r;
int a[N], b[N];
int rec, cnt, flag;
signed main()
{
	n = read(), l = read(), r = read();
	for(register int i = 1; i <= n; i++) a[i] = i;
	int sum = 0;
	for(register int x = 1; x <= n; x++){ //枚举 (x,y) 中的 x 
		//这一轮有 n - x 种不同的数 
		sum = sum + (n - x);
		if(sum >= l && !flag){ //刚刚进入 
			if(sum <= r){
				flag = 1, rec = x + 1;
				int mi = n - (sum - l);
				for(register int y = mi; y <= n; y++) swap(a[x], a[y]);
			}
			else{
				int mi = n - (sum - l), mx = n - (sum - r);
				for(register int y = mi; y <= mx; y++) swap(a[x], a[y]);
				break;
			}
		}
		else if(flag == 1 && sum < r) cnt++; //吃掉了一轮的数 
		else{
			if(flag == 1 && sum >= r){
				for(register int i = 1; i <= n; i++) b[i] = a[i];
				for(register int i = rec + cnt; i <= n; i++) a[i] = b[i - cnt];
				for(register int i = rec, j = n; i < rec + cnt; i++, j--) a[i] = b[j];
				int mx = n - (sum - r);
				for(register int y = x + 1; y <= mx; y++) swap(a[x], a[y]);
				flag = 2;
			}
		}
	}
	for(register int i = 1; i <= n; i++) printf("%lld ", a[i]);
	return 0;
}

\(Ex\)

\(Problem\)

给定 \(N\) 个点,以及两个长为 \(M\) 的序列 \(u\)\(v\)

执行以下操作 \(N-1\) 次:

  • 等概率的选择一个 \(i(1\leq i\leq M)\),连接点 \(u_i\)\(v_i\)

\(k\) 次操作后有多少概率这张无向图会是一个森林,并对 \(998244353\) 取模。

\(2\leq N\leq 14,N-1\leq M\leq 500,1\leq u_i,v_i\leq N,u_i\neq v_i\)

\(Sol\)

官方题解用到了矩阵树定理,但是我不会。有一个用户题解给出了不使用矩阵树定理的做法。

设集合 \(V=\{1,2,3……N \}\)

\(f_S\) 表示 \(S\subseteq V\) ,且以集合 \(S\) 中的点形成一棵树的方案数。

\(g_{S,i}\) 表示 \(S\subseteq V\),且以集合 \(S\) 中的点形成一片森林且边的数量为 \(i\) 的方案数。

那么,对于 \(k=i\),答案即是 $$\frac{g_{V,i}\times i!}{m^i}$$

我们只需考虑如何求解 \(g\) 数组。

首先考虑求解 \(f\) 数组。

事实上,\(f\) 数组满足一下递推关系:

  • \[f_S = 1,|S| \leq 1 \]

  • \[f_S = \frac{1}{2\times(|S|-1)}\sum_{T\subset S}f_T\times f_{S/T}\times val(T,S/T) \]

其中 \(S/T\) 表示 \(T\)\(S\) 范围下的补集。\(val(U,V)\) 表示点集 \(U\) 和点集 \(V\) 之间边的数量,\(val(U,V)\) 求解如下:

  • \(e_S\) 表示 \(u_i\)\(v_i\) 均在点集 \(S\) 中的边的数量。

  • \(val(U,V) = e(U\cup V)-e(U)-e(V)\)

这是个简单的容斥,不难理解。

简单分析一下 \(f\) 数组求解原理,即相当于两颗树通过连边合并到一起。之所以除以 \(2\times (|S| - 1)\)\(2\) 是因为我们通过计算补集递推 \(f_S\),则相同的状态我们会计算两次,\(|S|-1\) 是因为一个节点数为 \(|S|\) 的树有 \(|S|-1\) 条边,合并时我们分为两个部分合并,一个形态相同的树可以按照 \(|S|-1\) 个边断开得到 \(|S| -1\) 个不同的组合。

求得 \(f\) 数组后,即可递推 \(g\)

\[g_{S, i} = \sum_{T\subset S}f_{T}\times g_{S/T,i-(|T|-1)} \]

这是个合并的过程,相当于将一个 \(|T|\) 个节点的树合并到了一个森林中。

\(code\)

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1 << 14, M = 5e2 + 10, mod = 998244353;
inline int read()
{
	int s = 0, w = 1;
	char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
	return s * w;
}
int n, m;
int fac[N], siz[N];
int f[N], e[N], g[N][M]; //e[i] 表示子集 i 中共有多少种可能的边
inline int val(int x, int y) { return e[x | y] - e[x] - e[y]; } //求子集 x 和 y 中分别有多少边 
inline int power(int x, int k)
{
	int res = 1;
	while(k){
		if(k & 1) res = res * x % mod;
		x = x * x % mod, k >>= 1;
	}
	return res;
}
vector<int> vec[20];
signed main()
{
	n = read(), m = read(), fac[0] = 1;
	for(register int i = 1; i < (1 << n); i++) siz[i] = siz[i >> 1] + (i & 1);
	for(register int i = 1; i < n; i++) fac[i] = fac[i - 1] * i % mod;
	for(register int i = 1; i <= m; i++){ 
		int x = read(), y = read();		
		for(register int j = 0; j < N; j++){ //枚举子集 
			if(!(j & (1 << (x - 1))) || !(j & (1 << (y - 1)))) continue; 
			e[j]++;
		}
	}
	for(register int i = 1; i < (1 << n); i++) vec[siz[i]].push_back(i);
	f[0] = 1;
	for(register int i = 0; i < vec[1].size(); i++) f[vec[1][i]] = 1; //集合大小为 1 
	for(register int t = 2; t <= n; t++){ //枚举集合大小 
		for(register int i = 0; i < vec[t].size(); i++){ //枚举集合
			int res = 0, s = vec[t][i];
			for(register int a = (s - 1) & s; a ; a = (a - 1) & s){ //枚举 vec[t][i] 下的子集
				int b = s ^ a;
				res = (res + f[a] * f[b] % mod * val(a, b) % mod) % mod;
			}
			f[s] = res * power(2 * (t - 1), mod - 2) % mod;
		}
	}
//	for(register int i = 0; i < (1 << n); i++) cout << f[i] << "\n";
	for(register int i = 0; i < n; i++){
		for(register int s = 0; s < (1 << n); s++){
			if(i == siz[s] - 1) g[s][i] = f[s];
			int p = s & (-s);
			for(register int a = s; a; a = (a - 1) & s){
				if((a & p) == 0) continue;
				if(i - siz[a] + 1 >= 0)
					g[s][i] = ((f[a] * g[s ^ a][i - siz[a] + 1]) % mod + g[s][i]) % mod;
			}
			//cout << i << " " << s << " " << g[s][i] << endl;
		}
	}
	int v = (1 << n) - 1;
	for(register int i = 1; i < n; i++){
		printf("%lld\n", g[v][i] * fac[i] % mod * power(power(m, i), mod - 2) % mod);;
	}
	return 0;
}
 
posted @ 2022-05-29 18:13  ╰⋛⋋⊱๑落叶๑⊰⋌⋚╯  阅读(58)  评论(0编辑  收藏  举报