2022 CCPC 广州站

2022 CCPC 广州站

L. Station of Fate【计数】

Description

有 n 个人,m 个站台,排成了 m 条队伍。

需要保证每条队伍至少有 1 个人,求总方案数。

两个方案不同需要至少满足下述一种情况:① 有站台的队伍所含的人不同 ② 人虽相同,但排队顺序不同。

Solution

考虑将 n 个人排成一列,在里面放 m-1 个隔板来分成 m 条队伍。

对于 n 个人的每一种排列都统计一次答案即可得所有方案。

mayb 想问题的时候可以倒着想?对于每一种不同的排队方案,将它们按照站台顺序排成一个序列,两两不同的方案一定要么最后的序列互不相同,要么是序列相同但是分割点不同。所以只需要讨论所有的序列和所有可能的分割就行。

H. GameX 【贪心】

Description

给定一个长度为 n 的自然数序列 S。

现在 Alice 和 Bob,每轮操作分别向序列中插入一个数。若 k 轮操作后,MEX(S) 是偶数,则 Alice赢;否则 Bob 赢。

说明:MEX(S) 指的是不在序列 S 中的最小自然数。

Solution

Alice每次向序列中插入最小的不在序列中的奇数,

Bob每次向序列中插入最小的不在序列中的偶数。

E. Elevator【二位数点/树状数组】

Description

有 n 个电梯,m 层楼。每秒,电梯上升 1 层。

在所有电梯开始运行前,可以在任意楼层(除 1 , m)按电梯。但是注意,一旦有电梯到达这个楼层,该电梯会停 1 秒,但是后面来的电梯都不会停了。若同时有多个电梯到达,则序号最小的作为第 1 个到达的。

给定所有电梯的出发时间,所有电梯都从第 1 层出发,求最少的按电梯次数使得第 i 个电梯第一个达到 m 层。

对于从 1 到 n 所有的 i,都输出一个答案。(若无答案则输出 -1)

Solution

首先注意到最多只能按 m2 次电梯。

对于 i,答案分为序号比它小的部分和序号比它大的部分,

​ 对于序号小于 i 的部分,答案为 j=1i1(x[j]x[i])(x[i]+1x[j])

​ 对于序号大于 i 的部分,答案为 j=i+1n(x[j]<x[i])(x[i]x[j])

考虑先求 j=1n(x[j]<x[i])(x[i]x[j]),再求j=1i1(x[i]x[j])1

前面一个式子非常好求,直接排序求个前缀和再处理下就能出来,重点是第二个式子怎么求。

注意到这是一个二维数点问题,求关键值1 和 关键值 2 都小于一个点的所有点。放到坐标系中理解,相当于求一个点左下角的所有点(满足横纵坐标均不大于该点)。

树状数组求。按照x[i]为第一关键字,id为第二关键字的顺序从小到大将电梯加入树状数组,树状数组以 id 为下标。每次加入一个电梯前(后也行),统计一个sum(id1)就是所求的值(第二个式子的值)了。

ps: 输入输出数据量大,不要直接使用 cin 和 cout。

M. XOR Sum 【数位dp/计搜】

Description

一个长度为 k 的非负整数的序列 Avalue 定义为: i=1kj=1i1ai XOR aj

给定 Avaluen,限制0aim

求满足以上条件的不同的序列 A 的个数。

Solution

将数二进制展开,按位考虑。

发现对于 2p 位,对 value 的贡献为在该位上,01对的数目乘上 2p,因为只有 0 XOR 1=1

考虑从高位到低位枚举统计答案,需要注意的是由于有 m 的限制,所以我们还需要记录一个当前有多少个数卡在上限的参数。

dfs(digi,lim,res) 表示现在在2digi位,有lim个数达到m的上限,还需要的valueres

mdigi 位上为 1,则可以从受限制的 lim 个数中选 i 个,剩下不受限制的 nlim 个数中选 j 个,把选出来的 i+j 个数的这一位置为 1, 这样贡献的 value(i+j)(kij),方案数为ClimiCklimjdfs(digi+1,i,resval)

mdigi 位上为 0,则只可以从不受限制的 nlim 个数中选 j 个数,将此位置为 1,这样贡献的 valuej(kj),方案数为Cklimjdfs(digi+1,lim,resval)

需要特别说明的是,由于需要记忆化,记录 res 有两种方法:① 开一个 map<pair<int,int>>f[][],数组的下标为 digilimmap 的值的 first 对应 ressecond 记录方案数;② 直接开三维数组,但是处理一下,只考虑不低于 digi 位的 res,这个res 是个相对值(比如说 res 原本应该是 [1001011]2,现在考虑到22,那么我们记录的 res[10010]2 ,在搜索下一位时才加上后面的数位)。注意采用这种方法的话,贡献是不需要乘上 2digi 的。

找点性质剪枝,注意到 k 个数能产生最大贡献为 k/2k/2,若 k/2k/2+k/2k/21<res,则最后一定是不合法的,可以直接剪掉。如何理解这一点:因为在当前位没有减去的 res,如果累计到下一位至少会是 res2 ,如果次位最少都会剩余 k/2k/2,也就是单个位能产生的最大贡献,那么后面肯定不能将res全部消去。

由于 k18,所以res992=162

则总的状态数为(log2m)k162,最多为 4018162。但是注意到在搜索函数中枚举了 i,j,所以总的复杂度应该是4099162=524880。(应该是这样8,欢迎指正!!)

一定要注意考虑一些边界情况、特殊情况!比如 k=1m=1n=0

关键代码见下,完整代码见Code板块。

ll m, n;
//m : 限制数列元素的范围不大于 m ;n:A的题目定义的val=n
//int dm[M]; //记录 m 的每一位是多少
int k, C[M][M]; //数列的长度; 组合数数组
int f[M][M][N]; //另一种方式,详见solution
int dfs(int digi, int lim, int res) { 
	//现在在2^digi位,有lim个数达到m的上限,高于等于这个位的还需要的value为 res
	if (digi < 0) return res == 0;
	if (res < 0) return 0;
	if ((k / 2) * ((k + 1) / 2) * 2 - 1 < res) return 0; //k < 2 时,这个式子无法成立
	if (f[digi][lim][res] != -1) return f[digi][lim][res];
	int ret = 0;
	int add_res = digi == 0 ? 0 : n >> (digi - 1) & 1;
	if (m >> digi & 1) {
		for (int i = 0;i <= lim;++i) //在受限制的数中选 i 个置为 1
			for (int j = 0;j <= k - lim;++j) {  //在不受限制的数中选 j 个置为 1
				int val = (i + j) * (k - i - j); //这样选产生的贡献value
				if (val > res) continue;
				ret += 1ll * C[lim][i] * C[k - lim][j] % mod * dfs(digi - 1, i, (res - val) << 1 | add_res) % mod;
				ret %= mod;
			}
	}
	else {
		for (int j = 0;j <= k - lim;++j) {  //在不受限制的数中选 j 个置为 1
			int val = j * (k - j); //这样选产生的贡献value
			if (val > res) continue;
			ret += 1ll * C[k - lim][j] * dfs(digi - 1, lim, (res - val) << 1 | add_res) % mod;
			ret %= mod;
		}
	}
	return f[digi][lim][res] = ret;
}

其他:用杨辉三角形预处理组合数!

I. Infection【树形dp/换根dp】

Description

有一种细菌正在感染一棵树的 n 个结点!

结点 i 成为整棵树第一个被感染的结点的概率为 aij=1naj

若结点 i 有相邻的结点已经被感染,则它被感染的概率为 pi

求最后整棵树恰好有 k 个结点被感染的概率(mod),k[1,n]

Solution

f[u][i][0/1] 为在以 u 为根的子树中有 i 个结点被感染(理解为包含 u 且所有点都在子树内的连通块),其中还没有/已经选择了初始感染结点的概率。

转移:枚举 u 的子结点 v,枚举从 u 已经考虑过的子树和 v 的子树中分别选择多少个感染的结点,设为 i,j,则转移方程为:

f[u][i+j][0]+=f[u][i][0]f[v][j][0]

f[u][i+j][1]+=f[u][i][0]f[v][j][1]+f[u][i][1]f[u][j][0]

其中 i1 开始取,而 j0 开始取。

特别需要注意 f[u][0][0]=1p[u],既可以理解为当 u 的父结点被感染时,u 的子树都不被感染的改概率,也可以理解为当 u 的子结点被感染时,u 以及除了被感染的子树之外其他地方都不被感染到的概率。

答案的累计:ans[i]+=f[u][i][1]f[u][0][0],一定要记得乘上 f[u][0][0]

细节:① 转移的时候需要一个辅助数组,不能直接改变 f[u][i+j][0/1],因为这样会造成转移错误。

​ ② size[]tricksz[u] 记录以 u 为根的子树里,当前统计到的最多的感染结点数,可以避免不必要的枚举,优化时间复杂度。

​ ③ 注意初始化 f[0][0][0]=1

关键代码:

void dfs(int u, int fa) {
	a[u] = 1ll * a[u] * tot_a % mod;
	f[u][1][0] = p[u];
	f[u][1][1] = a[u];
	f[u][0][0] = (1 - p[u] + mod) % mod;  //!!!
	sz[u] = 1;

	for (int v : to[u]) {
		if (v == fa) continue;
		dfs(v, u);
		for (int i = 1;i <= sz[u];++i)
			for (int j = 0;j <= sz[v];++j) {
				tmp[i + j][0] += 1ll * f[u][i][0] * f[v][j][0] % mod;
				tmp[i + j][0] %= mod;
				tmp[i + j][1] += (1ll * f[u][i][0] * f[v][j][1] % mod + 1ll * f[u][i][1] * f[v][j][0] % mod) % mod;
				tmp[i + j][1] %= mod;
			}
		sz[u] += sz[v];
		for (int i = 1;i <= sz[u];++i) {
			f[u][i][0] = tmp[i][0];
			f[u][i][1] = tmp[i][1];
			tmp[i][0] = tmp[i][1] = 0;
		}
	}

	for (int i = 1;i <= sz[u];++i)
		ans[i] = (ans[i] + 1ll * f[u][i][1] * f[fa][0][0] % mod) % mod;  
}

C. Customs Controls 2 【图/拓扑排序/并查集】

Description

给定一个有向无环图 G,你需要给每个结点 i 分配一个正数的权值 wi,使得从结点 i 到结点 n 的所有路径的长度都相同。定义路径的长度为路径上所有结点的权值之和。

数据保证无多重边、自环、环,保证所有的点都可以从结点 1 到达,也都可以到达 n

Solution

首先进行一些性质的挖掘,发现如果存在若干个结点都有向同一个结点的连边(例如:存在 u1v,u2v,u3v ),那么这些结点到结点 1 的路径长度都要相同(dis[u1]=dis[u2]=dis[u3])。

考虑将 dis 需要相同的点集缩成一个点。全部缩完之后重新建图,若该图有环,则无解。用拓扑排序来检验新图是否有解,并且顺便计算每个点的层数(理解为最长 dis ?其实就是拓扑的顺序这样是为了保证每条边(u,v)dis[u]<dis[v],换言之,为了保证 v 的权值为正)。

最后答案即为每个结点的 拓扑序 减去有边连向该结点的结点的 拓扑序(连向同一个结点的所有结点在同一缩点点集中,它们的拓扑序都相同,这也是缩点的原因!为了使得结点能求出唯一的权值),这样一来,所有路径的长度都相同。

缩点的具体方法:建反图,所有的 u 连向的 v 都放入一个点集,用并查集维护。

感觉还是有点抽象,模拟下样例感受一下 8

Code

L. Station of Fate【计数】

//by dttttttt
#include<iostream>
using namespace std;
const int N = 1e5 + 5, mod=998244353;
int fac[N];
void init_fac() {
	fac[0] = 1;
	for (int i = 1;i < N - 3;++i)
		fac[i] = 1ll * fac[i - 1] * i % mod;
}
int inv(int x) {
	int y = mod - 2;
	int res = 1;
	while (y) {
		if (y & 1)
			res = 1ll * res * x % mod;
		x = 1ll * x * x % mod;
		y >>= 1;
	}
	return res;
}
int C(int n, int m) {
	return 1ll * fac[n] * inv(fac[m]) % mod * inv(fac[n - m]) % mod;
}
int main() {
	init_fac();
	int T, n, m;
	cin >> T;
	while (T--) {
		cin >> n >> m;
		cout << 1ll * fac[n] * C(n - 1, m - 1) % mod << endl;
	}
	return 0;
}

H. GameX 【贪心】

// by zrx & dtt
#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

const int maxn = 200005;
int a[1000005];
int k, n;

int main()
{
    int T;
    cin >> T;
    for (int TT = 1;TT <= T;++TT)
    {
        cin >> n >> k;
        int t;
        for (int i = 0;i < n;i++)
        {
            cin >> t;
            a[t] = TT;
        }

        int cnt = 0;
        int x = 1;//外面最小的奇数
        int y = 0;//外面最小的偶数
        while (cnt < 2 * k)
        {
            if (cnt % 2 == 0)//轮到Alice删奇�?
            {
                while (a[x] == TT)//若x在S�?
                {
                    x += 2;
                }
                x += 2;
                while (a[x] == TT)//若x在S�?找到删去后最小的不在S内的奇数
                {
                    x += 2;
                }
            }
            else//轮到Bob删偶�?
            {
                while (a[y] == TT)//若x在S�?
                {
                    y += 2;
                }
                y += 2;
                while (a[y] == TT)//若x在S�?找到删去后最小的不在S内的奇数
                {
                    y += 2;
                }
            }
            cnt++;
        }
        if (x < y)//S外奇数大于偶�?
        {
            cout << "Bob" << endl;
        }
        else {
            cout << "Alice" << endl;
        }
    }
    return 0;
}

E. Elevator【二位数点/树状数组】

//by dttttttt
#include<iostream>
#include<algorithm>
#include<cstring>

#define ll long long
using namespace std;
const int N = 5e5 + 5;
int n, m, L[N], t[N]; //L[i]: �� i �����xС�ڵ�����������
ll ans[N], sum[N];
struct node {
	int id, x, x_dis;
}a[N];

bool cmp(node x, node y) {
	if (x.x == y.x) return x.id < y.id;
	return x.x < y.x;
}
bool cmp_id(node x, node y) {
	return x.id < y.id;
}
int lowbit(int x) {
	return x & (-x);
}
void t_add(int x) {
	while (x <= n) {
		++t[x];
		x += lowbit(x);
	}
}
int t_sum(int x) {
	int ret = 0;
	while (x) {
		ret += t[x];
		x -= lowbit(x);
	}
	return ret;
}

int main() {
	cin >> n >> m;
	for (int i = 1;i <= n;++i) {
		scanf("%d", &a[i].x);
		a[i].id = i;
	}

	sort(a + 1, a + n + 1, cmp);
	for (int i = 1;i <= n;++i) {
		sum[i] = sum[i - 1] + a[i].x;
		a[i].x_dis = i;
		t_add(a[i].id);
		L[a[i].id] = t_sum(a[i].id - 1);
	}

	memset(ans, -1, sizeof(ans));
	for (int i = 1;i <= n;++i) {
		int j = i;
		while (j < n && a[j].x == a[j + 1].x) ++j;
		ll tmp = 1ll * (j - 1) * a[j].x - sum[j - 1];
		if (tmp > m - 2) break;
		while (i <= j) {
			ans[a[i].id] = tmp + L[a[i].id];
			++i;
		}
		--i;
	}

	for (int i = 1;i <= n;++i)
		if (ans[i] <= m - 2) printf("%lld\n", ans[i]);
		else puts("-1");
	return 0;
}

M. XOR Sum 【数位dp/计搜】

//by dttttttt
#include<iostream>
#include<cstring>
#define ll long long
using namespace std;
const int M = 42, N = 200, mod = 1e9 + 7;
ll m, n;
//m : 限制数列元素的范围不大于 m ;n:A的题目定义的val=n
//int dm[M]; //记录 m 的每一位是多少
int k, C[M][M]; //数列的长度; 组合数数组
int f[M][M][N]; //另一种方式,详见solution
int dfs(int digi, int lim, int res) { 
	//现在在2^digi位,有lim个数达到m的上限,高于等于这个位的还需要的value为 res
	if (digi < 0) return res == 0;
	if (res < 0) return 0;
	if ((k / 2) * ((k + 1) / 2) * 2 - 1 < res) return 0; //k < 2 时,这个式子无法成立
	if (f[digi][lim][res] != -1) return f[digi][lim][res];
	int ret = 0;
	int add_res = digi == 0 ? 0 : n >> (digi - 1) & 1;
	if (m >> digi & 1) {
		for (int i = 0;i <= lim;++i) //在受限制的数中选 i 个置为 1
			for (int j = 0;j <= k - lim;++j) {  //在不受限制的数中选 j 个置为 1
				int val = (i + j) * (k - i - j); //这样选产生的贡献value
				if (val > res) continue;
				ret += 1ll * C[lim][i] * C[k - lim][j] % mod * dfs(digi - 1, i, (res - val) << 1 | add_res) % mod;
				ret %= mod;
			}
	}
	else {
		for (int j = 0;j <= k - lim;++j) {  //在不受限制的数中选 j 个置为 1
			int val = j * (k - j); //这样选产生的贡献value
			if (val > res) continue;
			ret += 1ll * C[k - lim][j] * dfs(digi - 1, lim, (res - val) << 1 | add_res) % mod;
			ret %= mod;
		}
	}
	return f[digi][lim][res] = ret;
}
void init_C() { //预处理组合数
	for (int i = 0;i <= 18;++i) {
		C[i][0] = 1; //不要落掉这个!
		for (int j = 1;j <= i;++j) 
			C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
	}
}
int main() {
	init_C();
	memset(f, -1, sizeof(f));
	cin >> n >> m >> k;
	int digit = 0;
	ll tmp_m = m;
	while (tmp_m / 2) ++digit, tmp_m /= 2;

	if (k == 1)  //注意考虑特殊情况
		cout << (n == 0 ? m + 1 : 0) << endl;
	else
		cout << dfs(digit, k, n >> digit) << endl;
	return 0;
}

I. Infection【树形dp/换根dp】

//by dttttttt
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
const int N = 2e3 + 5, mod = 1e9 + 7;
int n, p[N], a[N], f[N][N][2], tot_a, sz[N], tmp[N][2], ans[N];
vector<int>to[N];
int inv(int x) {
	int y = mod - 2;
	int ret = 1;
	while (y) {
		if (y & 1) 
			ret = 1ll * ret * x % mod;
		x = 1ll * x * x % mod;
		y >>= 1;
	}
	return ret;
}
void dfs(int u, int fa) {
	a[u] = 1ll * a[u] * tot_a % mod;
	f[u][1][0] = p[u];
	f[u][1][1] = a[u];
	f[u][0][0] = (1 - p[u] + mod) % mod;  //!!!
	sz[u] = 1;

	for (int v : to[u]) {
		if (v == fa) continue;
		dfs(v, u);
		for (int i = 1;i <= sz[u];++i)
			for (int j = 0;j <= sz[v];++j) {
				tmp[i + j][0] += 1ll * f[u][i][0] * f[v][j][0] % mod;
				tmp[i + j][0] %= mod;
				tmp[i + j][1] += (1ll * f[u][i][0] * f[v][j][1] % mod + 1ll * f[u][i][1] * f[v][j][0] % mod) % mod;
				tmp[i + j][1] %= mod;
			}
		sz[u] += sz[v];
		for (int i = 1;i <= sz[u];++i) {
			f[u][i][0] = tmp[i][0];
			f[u][i][1] = tmp[i][1];
			tmp[i][0] = tmp[i][1] = 0;
		}
	}

	for (int i = 1;i <= sz[u];++i)
		ans[i] = (ans[i] + 1ll * f[u][i][1] * f[fa][0][0] % mod) % mod;  
}
int main() {
	scanf("%d", &n);
	for (int i = 1;i < n;++i) {
		int u, v;
		scanf("%d%d", &u, &v);
		to[u].push_back(v);
		to[v].push_back(u);
	}
	for (int i = 1;i <= n;++i) {
		int ai, bi, ci;
		scanf("%d%d%d", &ai, &bi, &ci);
		a[i] = ai;
		tot_a = (tot_a + a[i]) % mod;
		p[i] = 1ll * bi * inv(ci) % mod;
	}

	tot_a = inv(tot_a);

	f[0][0][0] = 1;  //注意这里的边界!!
	dfs(1, 0);

	for (int i = 1;i <= n;++i)
		cout << ans[i] << endl;

	return 0;
}

C. Customs Controls 2 【图/拓扑排序/并查集】

//by dttttttt
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
using namespace std;
const int N = 2e5 + 5, M = 5e5 + 5;
int n, m, fa[N], in[N], dep[N];
vector<int> to[N], inv_to[N], mer_to[N];
queue<int> q; //用于拓扑排序
void init() {
	while (q.size()) q.pop();
	for (int i = 1;i <= n;++i) {
		to[i].clear(), inv_to[i].clear();
		mer_to[i].clear();
		fa[i] = i;
		in[i] = dep[i] = 0;
	}
}
int find(int x) {
	return x == fa[x] ? x : fa[x] = find(fa[x]);
}
int main() {
	int T; scanf("%d", &T);
	while (T--) {
		scanf("%d%d", &n, &m);
		init(); //多组数据,进行一些初始化
		for (int i = 1;i <= m;++i) {
			int u, v;
			scanf("%d%d", &u, &v);
			to[u].push_back(v);
			inv_to[v].push_back(u);
		}

		//下面开始合并点
		for (int i = 1;i <= n;++i) {
			if (inv_to[i].size() == 0) continue;
			int u = inv_to[i][0];
			for (int v : inv_to[i]) {
				int fu = find(u), fv = find(v);
				fa[fu] = fv;
			}
		}

		//下面按照缩点之后的情况重新构图、计算入度
		for (int u = 1;u <= n;++u) {
			for (int v : to[u]) {
				mer_to[find(u)].push_back(find(v));
				++in[find(v)];
			}
			cout << "find(" << u << ")=" << find(u) << endl;
		}

		//下面开始拓扑排序,计算层度dep
		for (int i = 1;i <= n;++i)
			if (i == find(i) && in[i] == 0) 
				q.push(i);

		dep[1] = 1;
		while (q.size()) {
			int u = q.front();
			q.pop();
			for (int v : mer_to[u]) {
				if (--in[v] == 0) {
					q.push(v);
					dep[v] = dep[u] + 1;
				}
			}
		}

		bool tag = 1;
		for (int i = 1;i <= n;++i)
			if (i == find(i) && in[i] > 0) {
				tag = 0;
				break;
			}
		if (!tag) 
			puts("No");
		else {
			puts("Yes");
			printf("1 ");
			for (int i = 2;i <= n;++i)
				printf("%d ", dep[find(i)] - dep[find(inv_to[i][0])]);
			printf("\n");
		}
	}
	return 0;
}
posted @   DTTTTTTT-  阅读(309)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示