2025“钉耙编程”中国大学生算法设计春季联赛(3)
反思
\(\qquad\) 赛时只过了一题,被两道定理题卡住了,一道不知道定理,一道代码实现不出来。结果赛后发现另一道模拟其实能做。以后还是得把题都看一遍,不能只看榜单qwq。
1001 数列计数
数论+位运算
/*
思路:
我们需要让累乘之后的数字为奇数,那么对于每一个 C(ai, bi) 都只能为奇数。
因此我们只需要找到 li 范围内有多少个可以使 C(ai, bi) 为奇数的数然后累乘取模即可。
那我们如何判断一个组合数的奇偶性呢?就要引出 lucas 定理了。
lucas 定理:
对于素数 p 存在:
C(n, k) % p == C(n/p, k/p)*C(n%p, k%p) % p;
而对于判断奇偶性,显然 p = 2 ,此时 lucas 定理便可以变形为:
C(n, k) % 2 == C(n/2, k/2)*C(n%2, k%2) % 2;
通过观察,我们可以发现,对于一个组合数 C(n, k) % 2 其实就是对其进行二进制拆分,
并且累乘的结果,即我们假设 n, k 最高位为第 m 位,那么就可以简单表示为:
C(n, k) % 2 == C(n0, k0)*...*C(ni, ki)*...*C(nm, km); (0 <= i <= m);
然后我们发现,对于 C(n, k) % 2 二进制拆分后的每一位,我们有四种情况:
C(1, 1), C(1, 0), C(0, 1), C(0, 0);
而其中只有当 C(0, 1) 时结果为 0;
所以我们得出结论,对于组合数 C(n, k) % 2 == 1 我们需要使 k 在二进制表示下的 0, 1 数列被 n 严格包含。
即:只有 n 的第 i 位为 1 时,k 的第 i 位才能考虑为 1 的情况。
在明确 ai | bi == ai 这一点后,我们就要开始考虑限制条件 li 了。
对此,我们可以对 ai 与 li 同时从最高位开始提取,就会出现四种情况,下面对这四种情况进行分类讨论:
我们定义 ak, lk 分别为 ai, li 当前提取出的二进制数字。
1) lk == 0 时,我们发现 ak 上无论是 1 还是 0 都不会对计数产生贡献。
2) lk == 1, ak == 1 时,我们简单地把 ak 位上的数字当作 0, 此时 0~k-1 位上的 1 不论如何都能对计数产生贡献。
3) 1k == 1, ak == 0 时,此时,我们便可以隐式判断当情况 2) 出现时 ak 取 1 的计数:
我们假设 1101(13) 和 1011(11) 在比较第 3 位时,我们发现 a3 = l3 = 1 ,由 2) 的判断,我们将 0101,0001,0100,0000 加入计数。
而比较到第 2 位时,因为 l2 = 0,因此跳过判断。
比较到第 1 位时,我们发现 l1 = 1, 而 a1 = 0, 此时,我们发现在第 0 位上的数字不论取何值,加上第 3 位上的数字都不会超过 li,
因此我们将 1001, 1000 加入计数,同时退出计数。
PS:在遍历过程中,即使没有遇到 3) 的情况,也会由 2) 以同样的原理隐式判断上一次的 2)。
4) 当遍历到第 0 位的二进制比较都无法满足 3) 时,我们发现遍历过程中最近的一次 2) 并没有被隐式判断。也就是说可能会出现 (a) 101, (l) 100的情况。
我们便要对计数 +1 来将类似 100 的情况加入计数。
*/
#include <bits/stdc++.h>
using namespace std;
#define int long long
using ll = long long;
using ull = unsigned long long;
using uint = unsigned;
using pii = pair<int, int>;
const int MOD = 998244353;
int calc(int x, int y){
int res = 0;
int cnt = __builtin_popcount(x); //计算 x 在二进制表示下有多少个 1
for(int i = 30; i >= 0; i -- ){ //从最高位开始提取
int tx = (x >> i) & 1, ty = (y >> i) & 1; //提取 x, y 当前位的数。
if(tx) cnt--; //如果 x 当前位为 1 则说明当前位置往前还剩下 cnt-1 个 1
if(!ty) continue; //如果 y 当前位为 0 则跳过
if(tx) res += (1 << cnt); //如果 y 与 x 的当前位都是 1 则对往前位置的每一个 1 计算贡献加入计数
if(!tx) return res + (1 << cnt); //如果 y 的当前位为 1,但 x 的当前位为 0 则将当前位的所有 1 计算贡献并退出程序
}
return res + 1; //加上最后一个没有被隐式判断的 1
}
void solve(){
int n;
cin >> n;
vector<int> a(n+1), l(n+1);
for(int i = 1; i <= n; i ++ ) cin >> a[i];
for(int i = 1; i <= n; i ++ ) cin >> l[i];
int ans = 1;
for(int i = 1; i <= n; i ++ ){
ans = 1LL * ans * calc(a[i], l[i]) % MOD; //累乘并取模
}
cout << ans << endl;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1;
cin >> t;
while(t--){
solve();
}
return 0;
}
1003 拼尽全力
优先队列+模拟
/*
思路:
如果一味的使用贪心策略来排序面试顺序,需要考虑比较的元素很多,而且不好实现,因此我们思考如何将问题简化。
首先,我们可以创建一个队列 list 来表示面试的顺序。
根据题意,我们了解到,当小x的 m 个能力都 >= 公司所需要的 m 个能力时,面试会通过。
也就是说,我们可以将其看作每个公司有 m 个需要达标的能力,当且仅当 m == 0 时,该公司加入面试队列 list。
因此,我们可以维护一个数组 unable 记录下每一轮面试后各个公司还有多少需要达标的能力值,
若对于第 i 家公司,need[i] == 0 那么就将第 i 家公司加入面试队列 list。
而对于那些还未达标的能力值,我们可以通过一个二维小根堆来进行排序,
need[i].first 记录第 i 个能力达标所需要的值
need[i].second 记录这个值所代表的公司
在对面试队列进行遍历时,我们计算并储存面试结束后小x的 m 个能力的值,
并对 need 进行遍历,如果能力已经达标则弹出 need[i] 的首元素,并让这个元素所属的公司 unable 数量减一。
如果 unable == 0 则让该公司加入队列 list。
最后比较一下入队总数量和 n 的大小即可得出答案。
*/
#include <bits/stdc++.h>
using namespace std;
//#define int long long
using ll = long long;
using ull = unsigned long long;
using uint = unsigned;
using pii = pair<int, int>;
void solve(){
int n, m;
cin >> n >> m;
vector<ll> a(m+1);
for(int i = 1; i <= m; i ++ ) cin >> a[i];
vector<vector<int>> c(n+1, vector<int>(m+1)), w(n+1, vector<int>(m+1)); //存储每一个公司的所需能力值和通过贡献。
for(int i = 1; i <= n; i ++ ){
for(int j = 1; j <= m; j ++ ) cin >> c[i][j];
for(int j = 1; j <= m; j ++ ) cin >> w[i][j];
}
queue<int> list; //面试队列
vector<priority_queue<pii, vector<pii>, greater<pii>>> need(m+1); //小根堆对每一个能力值下还没有达标的所有公司的所需能力值进行排序。
vector<int> unable(n+1); //每个公司还有多少个没有达标的能力值。
for(int i = 1; i <= n; i ++ ){ //第一次先全部扫一遍计算 unable 数组,并将一开始可以面试的公司加入队列。
for(int j = 1; j <= m; j ++ ){
if(c[i][j] > a[j]){
unable[i]++;
need[j].push({c[i][j], i});
}
}
if(!unable[i]) list.push(i);
}
int sum = 0; //计算出队次数。
while(!list.empty()){ //遍历队列
++sum;
int u = list.front(); list.pop();
for(int i = 1; i <= m; i ++ ){
a[i] += w[u][i]; //计算这一轮面试后的能力值大小
while(!need[i].empty() && a[i] >= need[i].top().first){ //只要当前能力值以满足所需能力值就弹出。
auto [c, v] = need[i].top(); need[i].pop();
if(--unable[v] == 0) list.push(v); //如果此公司所需达标的能力值已经为 0 则加入面试队列。
}
}
}
if(sum == n) puts("YES");
else puts("NO");
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1;
cin >> t;
while(t--){
solve();
}
return 0;
}
1005 修复公路
并查集
#include <bits/stdc++.h>
using namespace std;
#define int long long
using ll = long long;
using ull = unsigned long long;
using uint = unsigned;
using pii = pair<int, int>;
const int N = 3e5+10;
int f[N];
int find(int u){
while(u != f[u]){
u = f[u] = f[f[u]];
}
return u;
}
void solve(){
int n;
cin >> n;
for(int i = 1; i <= n; i ++ ) f[i] = i;
for(int i = 1; i <= n; i ++ ){
int x;
cin >> x;
if(i-x >= 1){
f[find(i-x)] = find(i);
}
if(i+x <= n){
f[find(i+x)] = find(i);
}
}
int ans = -1;
for(int i = 1; i <= n; i ++ ){
//cout << f[i] << ' ';
if(f[i] == i) ans++;
}
//cout << '\n';
cout << ans << endl;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1;
cin >> t;
while(t--){
solve();
}
return 0;
}
1009 部落冲突
并查集+映射关系
/*
思路:
通过读题我们可以发现这是一个连通性问题,考虑用并查集解决。
但是对于操作 2 和 3 并不能简单地利用并查集解决。
回顾并查集的逻辑,通过联通各个元素而形成一个个集合。并通过 find 函数来判断两个节点是否在同一个集合。
然后我们再来看题目。操作 1 使两个部落合并为一个联盟,操作 2 将野蛮人 a 移动到部落 b 中。
操作 3 交换部落 a 与 b 的所有野蛮人,也就是元素;操作 4 查询单个野蛮人属于几号部落。
我们可以发现,在整个题目中,需要使用并查集进行操作的只有对部落的合并和对野蛮人的查询。
也就是说,我们可以考虑建立一个野蛮人与部落编号的映射,当合并时,我们只需要合并两个部落,而不是对部落中的野蛮人进行操作。
而在查询时,我们则需要对野蛮人 a 当前所映射的部落编号进行查询,找到部落并查集的根节点,并输出。
然而还有一个问题,就是当操作 3 交换两个部落内的元素时,我们不可能将两个并查集遍历一遍并全部交换。
因此我们可以考虑交换两个集合中根节点的部落编号,从而实现转移。所以我们还需要维护一个数组作为部落编号的映射,即储存部落当前的编号应该是多少。
可能有点绕,直接看code吧。
*/
#include <bits/stdc++.h>
using namespace std;
//#define int long long
using ll = long long;
using ull = unsigned long long;
using uint = unsigned;
using pii = pair<int, int>;
const int N = 2e6+5;
int f[N], id[N], col[N], icol[N]; // f[]并查集的夫结点数组; id[]储存每一个野人所映射的部落编号;
// col[]储存每一个部落在实际交换后的编号; icol[]储存每一个部落所映射的野人。
int find(int x){
while(f[x] != x){
x = f[x] = f[f[x]];
};
return x;
}
void solve(){
int n, q;
cin >> n >> q;
for(int i = 1; i <= n; i ++ ) f[i] = id[i] = col[i] = icol[i] = i; // 一开始全部初始化为自身
int cnt = n;
while(q--){
int op, a, b;
cin >> op;
if(op == 1){ //操作 1 合并两个部落。PS:合并的是部落,并没有对单个野人进行操作!
cin >> a >> b;
a = find(icol[a]), b = find(icol[b]);
f[b] = a;
} else if(op == 2) { //操作 2 将野人 a 转移到部落 b,我们先为野人 a 创建一个新的部落,然后让这个新的部落与 b 合并。
cin >> a >> b;
id[a] = ++cnt;
f[cnt] = icol[b];
} else if(op == 3) { //交换两个部落中的野人,首先交换两个部落集合根节点所映射的部落编号,然后交换这两个部落中所映射的野人。
cin >> a >> b;
swap(col[find(icol[a])], col[find(icol[b])]);
swap(icol[a], icol[b]);
} else { //查找单个野人所属的部落编号。
cin >> a;
cout << col[find(id[a])] << endl;
}
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1;
cin >> t;
while(t--){
solve();
}
return 0;
}
1010 选择配送
几何距离+贪心
/*
思路;
题目要求寻找最远曼哈顿距离的最小值。
由定义可知:对于点 A(x1, y1), B(x2, y2) 曼哈顿距离 D = |x1-x2| + |y1-y2|;
但是由于曼哈顿距离需要考虑两个点的 x, y 值,计算过于繁琐且不好处理,因此我们引入另一个距离概念。
切比雪夫距离:
对于点 A(x1, y1), B(x2, y2) 我们定义其切比雪夫距离为 D = max(|x1-x2|, |y1-y2|);
而当我们对哈夫曼距离公式进行绝对值拆解时,我们会得到:
D = max{(x1-x2)+(y1-y2), (x1-x2)+(y2-y1), (x2-x1)+(y1-y2), (x2-x1)+(y2-y1)}
= max{(x1+y1)-(x2+y2), (x2-y2)-(x1-y1), (x1-y1)-(x2-y2), (x2+y2)-(x1+y1)}
= max{|(x1+y1)-(x2+y2)|, |(x1-y1)-(x2-y2)|};
我们将化简得到的式子与切比雪夫距离公式作比较,可以发现,化简后的式子相当与点 A'(x1+y1, x1-y1), B'(x2+y2, x2-y2) 之间的切比雪夫距离。
由此我们得出结论,点 A(x1, y1), B(x2, y2) 之间的曼哈顿距离 == 点 A'(x1+y1, x1-y1), B'(x2+y2, x2-y2) 之间的切比雪夫距离。
在得到这个结论之后,我们就可以通过二者之间的转换,把原本需要考虑 x, y 的问题看作只需要讨论 x 或 y 的最大值。
因此,对于每一个客户的坐标,我们取最大的 x+y, x-y 与最小的 x+y, x-y 分别为 maxx, maxy, minx, miny。
然后简单维护一个答案 ans ,在输入每一个配送站的坐标时,我们将配送站的坐标也化为切比雪夫映射坐标,
并对上一步的四个最值进行运算取最大值,最后与 ans 进行比较取最小值。
*/
#include <bits/stdc++.h>
using namespace std;
#define int long long
using ll = long long;
using ull = unsigned long long;
using uint = unsigned;
using pii = pair<int, int>;
const int inf = 1e18; //注意这里最好取大一点,一开始没看数据取了 1e9 听取WA声一片。
void solve(){
int n, m;
cin >> n >> m;
int minx, miny, maxx, maxy;
maxx = maxy = -inf, minx = miny = inf;
for(int i = 1; i <= n; i ++ ){ //输入客户坐标,转化为切比雪夫映射坐标,并找到四个最值。
int x, y;
cin >> x >> y;
minx = min(minx, x+y);
maxx = max(maxx, x+y);
miny = min(miny, x-y);
maxy = max(maxy, x-y);
}
int ans = inf;
for(int i = 1; i <= m; i ++ ){ //输入配送站坐标,转化为切比雪夫映射坐标,找到最大值并与 ans 进行比较。
int a, b;
cin >> a >> b;
int x = a+b, y = a-b;
int MAX = max(max(llabs(x-minx), llabs(x-maxx)), max(llabs(y-miny), llabs(y-maxy)));
ans = min(ans, MAX);
}
cout << ans << endl;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
int t = 1;
cin >> t;
while(t--){
solve();
}
return 0;
}

浙公网安备 33010602011771号