2022 CCPC 广州站
2022 CCPC 广州站
L. Station of Fate【计数】
Description
有 n 个人,m 个站台,排成了 m 条队伍。
需要保证每条队伍至少有 1 个人,求总方案数。
两个方案不同需要至少满足下述一种情况:① 有站台的队伍所含的人不同 ② 人虽相同,但排队顺序不同。
Solution
考虑将 n 个人排成一列,在里面放 m-1 个隔板来分成 m 条队伍。
对于 n 个人的每一种排列都统计一次答案即可得所有方案。
想问题的时候可以倒着想?对于每一种不同的排队方案,将它们按照站台顺序排成一个序列,两两不同的方案一定要么最后的序列互不相同,要么是序列相同但是分割点不同。所以只需要讨论所有的序列和所有可能的分割就行。
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 层出发,求最少的按电梯次数使得第 个电梯第一个达到 层。
对于从 1 到 n 所有的 i,都输出一个答案。(若无答案则输出 -1)
Solution
首先注意到最多只能按 次电梯。
对于 ,答案分为序号比它小的部分和序号比它大的部分,
对于序号小于 的部分,答案为 ;
对于序号大于 的部分,答案为
考虑先求 ,再求。
前面一个式子非常好求,直接排序求个前缀和再处理下就能出来,重点是第二个式子怎么求。
注意到这是一个二维数点问题,求关键值1 和 关键值 2 都小于一个点的所有点。放到坐标系中理解,相当于求一个点左下角的所有点(满足横纵坐标均不大于该点)。
用树状数组求。按照为第一关键字,为第二关键字的顺序从小到大将电梯加入树状数组,树状数组以 为下标。每次加入一个电梯前(后也行),统计一个就是所求的值(第二个式子的值)了。
ps: 输入输出数据量大,不要直接使用 cin 和 cout。
M. XOR Sum 【数位dp/计搜】
Description
一个长度为 的非负整数的序列 的 定义为:
给定 的 为 ,限制。
求满足以上条件的不同的序列 的个数。
Solution
将数二进制展开,按位考虑。
发现对于 位,对 的贡献为在该位上,01对的数目乘上 ,因为只有 。
考虑从高位到低位枚举统计答案,需要注意的是由于有 的限制,所以我们还需要记录一个当前有多少个数卡在上限的参数。
表示现在在位,有个数达到的上限,还需要的为 。
若 在 位上为 ,则可以从受限制的 个数中选 个,剩下不受限制的 个数中选 个,把选出来的 个数的这一位置为 , 这样贡献的 为 ,方案数为;
若 在 位上为 ,则只可以从不受限制的 个数中选 个数,将此位置为 ,这样贡献的 为 ,方案数为;
需要特别说明的是,由于需要记忆化,记录 有两种方法:① 开一个 ,数组的下标为 和 , 的值的 对应 , 记录方案数;② 直接开三维数组,但是处理一下,只考虑不低于 位的 ,这个 是个相对值(比如说 原本应该是 ,现在考虑到,那么我们记录的 为 ,在搜索下一位时才加上后面的数位)。注意采用这种方法的话,贡献是不需要乘上 的。
找点性质剪枝,注意到 个数能产生最大贡献为 ,若 ,则最后一定是不合法的,可以直接剪掉。如何理解这一点:因为在当前位没有减去的 ,如果累计到下一位至少会是 ,如果次位最少都会剩余 ,也就是单个位能产生的最大贡献,那么后面肯定不能将全部消去。
由于 ,所以。
则总的状态数为,最多为 。但是注意到在搜索函数中枚举了 ,所以总的复杂度应该是。(应该是这样,欢迎指正!!)
一定要注意考虑一些边界情况、特殊情况!比如 。
关键代码见下,完整代码见板块。
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
有一种细菌正在感染一棵树的 个结点!
结点 成为整棵树第一个被感染的结点的概率为 ;
若结点 有相邻的结点已经被感染,则它被感染的概率为 。
求最后整棵树恰好有 个结点被感染的概率(mod),。
Solution
记 为在以 为根的子树中有 个结点被感染(理解为包含 且所有点都在子树内的连通块),其中还没有/已经选择了初始感染结点的概率。
转移:枚举 的子结点 ,枚举从 已经考虑过的子树和 的子树中分别选择多少个感染的结点,设为 ,则转移方程为:
其中 从 开始取,而 从 开始取。
特别需要注意 ,既可以理解为当 的父结点被感染时, 的子树都不被感染的改概率,也可以理解为当 的子结点被感染时, 以及除了被感染的子树之外其他地方都不被感染到的概率。
答案的累计:,一定要记得乘上
细节:① 转移的时候需要一个辅助数组,不能直接改变 ,因为这样会造成转移错误。
② 的 , 记录以 为根的子树里,当前统计到的最多的感染结点数,可以避免不必要的枚举,优化时间复杂度。
③ 注意初始化 。
关键代码:
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
给定一个有向无环图 ,你需要给每个结点 分配一个正数的权值 ,使得从结点 到结点 的所有路径的长度都相同。定义路径的长度为路径上所有结点的权值之和。
数据保证无多重边、自环、环,保证所有的点都可以从结点 到达,也都可以到达 。
Solution
首先进行一些性质的挖掘,发现如果存在若干个结点都有向同一个结点的连边(例如:存在 ),那么这些结点到结点 的路径长度都要相同()。
考虑将 需要相同的点集缩成一个点。全部缩完之后重新建图,若该图有环,则无解。用拓扑排序来检验新图是否有解,并且顺便计算每个点的层数(理解为最长 ?其实就是拓扑的顺序,这样是为了保证每条边,,换言之,为了保证 的权值为正)。
最后答案即为每个结点的 拓扑序 减去有边连向该结点的结点的 拓扑序(连向同一个结点的所有结点在同一缩点点集中,它们的拓扑序都相同,这也是缩点的原因!为了使得结点能求出唯一的权值),这样一来,所有路径的长度都相同。
缩点的具体方法:建反图,所有的 连向的 都放入一个点集,用并查集维护。
感觉还是有点抽象,模拟下样例感受一下 。
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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现