2021牛客寒假算法基础集训营3 解题补题报告
A题 模数的世界
大致猜一下,肯定能构造出 \(x,y\) ,使得答案为 \(p-1\) 。(\(a=b=0\) 的时候没办法构造,答案只能为 \(0\))
现在尝试证明:令 \(a\not= b\) 且 \(a\geq b\) ,
-
\(b=0\) 的时候很容易构造:令 \(x=(p-1)m,y=(p-1)n\),我们现在尝试使得 \(\gcd(m,n)=1,x\%p=a,y\%p=0\)
显然可以令 \(n=p\),现在开始构造 \(m\) :\(x\%p=(pm-m)\%p=(-m)\%p=a\),所以 \(-m=kp+a\) ,也就是 \(m=-kp-a\), 不妨令 \(k=-1\),则 \(m=p-a\) 。显然 \(\gcd(p,p-a)=1\) 。
综上,可以构造 \(x=(p-a)(p-1),y=p(p-1)\)
-
\(b\not=0\) 的情况很难整:
令 \(x=(p-1)m,y=(p-1)n\),则易得 \(m=k_1p-a,n=k_2p-b\),保证 \(k_1,k_2\) 均为正整数。
那么现在就尝试证明:存在 \(k_1,k_2\) ,使得 \(\gcd(k_1p-a,k_2p-b)=1\) 。
这里有点难想(我建议考场上直接写一个暴力怼一下就完事了):
不妨令 \(k_2=1\),则 \(n=p-b\)
有一个不太显然的结论:对于 \(k_1=1,2,...,p-b\) ,\((k_1p-a)\%(p-b)\) 的值各不相同
证明:倘若存在 \(1 \leq i<j \leq p-b\) ,且 \(ip-a\) 与 \(jp-a\) 关于 \(p-b\) 同余,那么 \((j-i)p\) 是 \(p-b\) 的倍数。
显然 \(\gcd(p,p-b)=1\),又因为 \(1 \leq j -i < p-b\) ,故 \(\gcd(j-i,p-b)=1\) ,所以显然 \(\gcd((j-i)p,p-b)=1\) ,即两者必定互质,与假设矛盾,故不存在。
那么很显然结论成立,那么必然存在 \(1 \leq k_1 \leq p-b\) ,使得 \(k_1p-a\) 和 \(1\) 关于 \(p-b\) 同余。
那么原证明成立。
综上,得到结论:
- \(a=b=0\) 时,\(ans=0\)
- \(a\not=0\text{ or }b\not=0\) 时,\(ans=p-1\)
求解的话就直接 \(exgcd\) 了,或者想我一样直接枚举(复杂度 \(O(T(p - b))\))(逃
#include <bits/stdc++.h>
using namespace std;
#define LL long long
LL gcd(LL a, LL b) {
if (!b) return a;
return gcd(b, a % b);
}
void solve() {
LL a, b, p;
scanf("%lld%lld%lld", &a, &b, &p);
if (a == 0 && b == 0) { //第一次写了个 a=0,给我人都调傻了
printf("0 %lld %lld\n", p, p);
return;
}
//如果 a=0 或 b=0,那么在 i=1 的情况下就直接成立了
//所以这么写,以减少码量
for (LL i = 1; i <= p - b; ++i)
if (gcd(i * p - a, p - b) == 1) {
printf("%lld %lld %lld\n", p - 1, (p - 1) * (i * p - a), (p - 1) * (p - b));
return;
}
}
int main()
{
int T;
scanf("%d", &T);
while (T--) solve();
return 0;
}
B题 内卷
C题 重力坠击 (暴力DFS)
看这数据规模,捞的一,直接硬模拟就完事了
但是我一开始没上手写,因为理解错题意了,以为如果能消灭全部敌人的话,必须尽可能的少攻击,所以每次都会判定一下,这样复杂度就上来了,但实际上并没有这个限制,我们仅需要尽量的多消灭敌人,所以能攻击就攻击就完事了。
枚举 \(k\) 个坐标,然后对 \(n\) 个炸弹一一判定,总复杂度为 \(O(225^kn)\)。
#include<bits/stdc++.h>
using namespace std;
int n, k, R;
struct node {
int x, y, r;
} a[15];
bool check(int x, int y, int p) {
return (x - a[p].x) * (x - a[p].x)
+ (y - a[p].y) * (y - a[p].y)
<= (R + a[p].r) * (R + a[p].r);
}
int dx[5], dy[5], ans;
int calc() {
int cnt = 0;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= k; ++j)
if (check(dx[j], dy[j], i)) {
++cnt; break;
}
return cnt;
}
void dfs(int d) {
if (d == k + 1) {
ans = max(ans, calc());
return;
}
for (int i = -7; i <= 7; ++i)
for (int j = -7; j <= 7; ++j)
dx[d] = i, dy[d] = j, dfs(d + 1);
return;
}
int main()
{
scanf("%d%d%d", &n, &k, &R);
for (int i = 1; i <= n; ++i)
scanf("%d%d%d", &a[i].x, &a[i].y, &a[i].r);
dfs(1);
printf("%d", ans);
return 0;
}
D题 Happy New Year! (签到)
白给题,直接暴力或者找规律就行了
#include<bits/stdc++.h>
using namespace std;
int calc(int x) {
int ans = 0;
while (x)
ans += x % 10, x /= 10;
return ans;
}
int main()
{
int n;
cin>>n;
for (int i = n + 1; ; ++i)
if (calc(n) == calc(i)) {
printf("%d", i);
return 0;
}
}
E题 买礼物 (线段树)
说起来很惭愧,这些题目明明知道是数据结构题,但是完全不知道怎么转化成我们学过的数据结构(图论题好歹知道先建一个图)
开两个数组 \(Last\) 和 \(Next\) , \(Last_i\) 记录 \(i\) 前面最靠近 \(i\) 且值也等于 \(a_i\) 的点的位置,如果没有的话值就记为 \(0\),\(Next\) 同理,没有的话就记为 \(n+1\)(这玩意有点像链表)
到这里,我们就可以转化成线段树的操作了:
- 买一个东西:\(Next[Last[i]] = Next[i],Last[Next[i]] = Last[i],Last[i]=0,Next[i]=n+1\),单点操作
- 查询:对于区间 \([l,r]\) 里面的一个 \(i\),判断是否 \(Next[i] \leq r\)。那么也就是判断 \(\min\limits_{l\leq i \leq r}Next[i] \leq r\) 是否成立
单点修改+区间 \(\min\),这显然就是线段树的操作了
别问我咋想到这么建的,问就是看官方题解
#include<bits/stdc++.h>
using namespace std;
const int N = 500010;
struct SegmentTree {
struct node {
int val, l, r;
} a[N<<2];
inline int ls(int x) { return x<<1; }
inline int rs(int x) { return x<<1|1; }
void pushup(int x) {
a[x].val = min(a[ls(x)].val, a[rs(x)].val);
}
void build(int *arr, int x, int l, int r) {
a[x].l = l, a[x].r = r;
if (l == r) {
a[x].val = arr[l];
return;
}
int mid = (l + r) >> 1;
build(arr, ls(x), l, mid);
build(arr, rs(x), mid + 1, r);
pushup(x);
}
void change(int p, int val, int x = 1) {
if (a[x].l == a[x].r) {
a[x].val = val;
return;
}
int mid = (a[x].l + a[x].r) >> 1;
if (p <= mid) change(p, val, ls(x));
else change(p, val, rs(x));
pushup(x);
}
int query(int l, int r, int x = 1) {
if (l <= a[x].l && a[x].r <= r)
return a[x].val;
int mid = (a[x].l + a[x].r) >> 1;
int ans = 1e9 + 10;
if (l <= mid) ans = min(ans, query(l, r, ls(x)));
if (r > mid) ans = min(ans, query(l, r, rs(x)));
return ans;
}
}Last, Next;
int n, T, a[N];
//
const int MAX_A = 1000010;
int pos[MAX_A], Arr_Last[N], Arr_Next[N];
void ConstructTree()
{
//Last
for (int i = 0; i < MAX_A; ++i)
pos[i] = 0;
for (int i = 1; i <= n; ++i)
Arr_Last[i] = pos[a[i]], pos[a[i]] = i;
Last.build(Arr_Last, 1, 1, n);
//Next
for (int i = 0; i < MAX_A; ++i)
pos[i] = n + 1;
for (int i = n; i >= 1; --i)
Arr_Next[i] = pos[a[i]], pos[a[i]] = i;
Next.build(Arr_Next, 1, 1, n);
}
int main()
{
scanf("%d%d", &n, &T);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
ConstructTree();
while (T--) {
int opt, x, l, r;
scanf("%d", &opt);
if (opt == 1) {
scanf("%d", &x);
Next.change(Last.query(x, x), Next.query(x, x));
Last.change(Next.query(x, x), Last.query(x, x));
Next.change(x, n + 1);
Last.change(x, 0);
}
else if (opt == 2) {
scanf("%d%d", &l, &r);
puts(Next.query(l, r) <= r ? "1" : "0");
}
}
return 0;
}
F题 匹配串
G题 糖果 (图的遍历)
如果两个人 \(x,y\) 是朋友,那么他们的糖果数量只能相同。尽可能减少开支,就是每个人都拿 \(\max(a_x,a_y)\) 个糖果。这样就可以保证两个人都满足了最低需求,不互相冲突,而且我们花费最少。
如果 \(z\) 是 \(y\) 的朋友,这时候两个人都要拿 \(\max(\max(a_x,a_y),a_z)\) ,也就是 \(\max(a_x,a_y,a_z)\) 个糖果。
注意了,如果这时候 \(y\) 的糖果数量变了,那么 \(x\) 也要变,保持和 \(y\) 的糖果数量一致。
这时候,我们发现了朋友似乎具有传递性质:\(x\) 和 \(y\),\(y\) 和 \(z\) 分别是朋友,那么 \(x\) 和 \(z\) 也是朋友。
好在我交的快,前脚交,后脚出题人发公告,特意说明这题里面,朋友不具有传递性,也就是我们上面这条推论是错误的。
这个公告害人不浅,导致不少本来正确的人把代码改掉,在新的错误思路里面绕来绕去(就离谱)
实际上,出题人也没说错,朋友不具有传递性,具有传递性的,只是这个糖果的数量具有传递性(即使 \(x\) 和 \(z\) 不是朋友,但是他们在糖果分配上面还是要当作朋友来考虑的)(好一手文字游戏)
实际上,我们转化一下,把每个小朋友视作一个点,朋友关系视作一条无向边,那么在一个连通块里面的所有点都可以视为互为朋友,那么他们的糖果数量,必须是这个连通块里面的小朋友的最低糖果要求的最大值。
那么这题思路就很清楚了:建图,然后跑多次 \(DFS\) 来找出所有连通块,以及每块连通块内部的最大点权,最后合并输出即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n, m;
long long a[N];
vector<int> G[N];
int vis[N];
long long cnt, maxv;
void dfs(int x) {
vis[x] = 1, ++cnt, maxv = max(maxv, a[x]);
for (int i = 0; i < G[x].size(); ++i) {
int to = G[x][i];
if (!vis[to]) dfs(to);
}
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%lld", &a[i]);
for (int i = 1; i <= m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
long long ans = 0;
for (int i = 1; i <= n; ++i) {
if (vis[i]) continue;
cnt = 0, maxv = -1;
dfs(i);
ans += cnt * maxv;
}
printf("%lld", ans);
return 0;
}
H题 数字串 (模拟)
很显然,想要变换的话,只有两种方法:
-
拆:例如 \(w\) 拆成 \(bc\),\(p\) 拆成 \(af\) 之类
-
并:上面的逆操作,例如 \(ad\) 拆成 \(n\) 等等
另外,注意 \(j\) 和 \(t\) 这两个特殊字符,他们没法拆,也没法并
这题没啥大思路,就是纯模拟,代码有亿点小细节,烦的一
#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
string s1, ans;
int main()
{
cin>>s1;
ans = "";
bool flag = false;
for (int i = 0; i < s1.length(); ++i) {
char c = s1[i];
if (c <= 'j' || c == 't') ans += c;
else {
flag = true;
if (c == 'k') ans += "aa";
else if (c == 'l') ans += "ab";
else if (c == 'm') ans += "ac";
else if (c == 'n') ans += "ad";
else if (c == 'o') ans += "ae";
else if (c == 'p') ans += "af";
else if (c == 'q') ans += "ag";
else if (c == 'r') ans += "ah";
else if (c == 's') ans += "ai";
else if (c == 'u') ans += "ba";
else if (c == 'v') ans += "bb";
else if (c == 'w') ans += "bc";
else if (c == 'x') ans += "bd";
else if (c == 'y') ans += "be";
else if (c == 'z') ans += "bf";
}
}
if (flag) cout<<ans;
else {
ans = "";
int i = 0;
while (i < s1.length()) {
bool tmp = false;
if (i != s1.length() - 1 && flag == false) {
if (s1[i] == 'a' && s1[i+1] <= 'i') {
flag = true;
tmp = true;
if (s1[i+1] == 'a') ans += "k";
else if (s1[i+1] == 'b') ans += "l";
else if (s1[i+1] == 'c') ans += "m";
else if (s1[i+1] == 'd') ans += "n";
else if (s1[i+1] == 'e') ans += "o";
else if (s1[i+1] == 'f') ans += "p";
else if (s1[i+1] == 'g') ans += "q";
else if (s1[i+1] == 'h') ans += "r";
else if (s1[i+1] == 'i') ans += "s";
i++;
}
else if (s1[i] == 'b' && s1[i+1] <= 'f') {
flag = true;
tmp = true;
if (s1[i+1] == 'a') ans += "u";
else if (s1[i+1] == 'b') ans += "v";
else if (s1[i+1] == 'c') ans += "w";
else if (s1[i+1] == 'd') ans += "x";
else if (s1[i+1] == 'e') ans += "y";
else if (s1[i+1] == 'f') ans += "z";
i++;
}
}
if(!tmp) ans += s1[i];
++i;
}
if (flag) cout<<ans;
else cout<<-1;
}
return 0;
}
I题 序列的美观度 (DP/贪心)
法一:DP
记 \(dp_i\) 为以 \(i\) 位为结尾的子序列的美观度最大值,那么有转移方程:
\(dp_i= \begin{cases} \max dp_j + 1 &\text{if } j < i,a_i=a_j \\ \max dp_j &\text{if } j < i,a_i\not=a_j \end{cases}\)
(这种最基础的 \(DP\) 方程我居然都没有推出来,我感觉我可以埋了)
但是这个方程的朴素复杂度是 \(O(n^2)\) 会 \(T\),所以必须想办法优化。
下一个式子的维护比较方便,直接线性维护就好,主要是上面那个 \(nt\) 式子,我们必须开个桶来记录下(逃
典型的 \(O(n^2)\) 优化成 \(O(n)\) 的 \(DP\) 典范
#include<bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n, a[N], pos[N], dp[N];
int main()
{
//
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
//
int Maxv = 0;
memset(pos, -1, sizeof(pos));
for (int i = 1; i <= n; ++i) {
dp[i] = Maxv;
if (pos[a[i]] != -1)
dp[i] = max(Maxv, dp[pos[a[i]]] + 1);
pos[a[i]] = i;
Maxv = max(Maxv, dp[i]);
}
printf("%d", Maxv);
return 0;
}
法二:贪心
建议看官方题解,我说的不太明白,整体意思就是,尽量选相同数字,如果能选的话,尽量越近越好(好像有点懂,但是并不是很清楚代码上面如何实践
J题 加法和乘法 (数学博弈)
奇数+奇数=偶数,奇数+偶数=奇数,偶数+偶数=偶数
奇数*奇数=奇数,奇数*偶数=偶数,偶数*偶数=偶数
我们将数字对 \(2\) 取模,变成 \(0\) 和 \(1\),似乎也能找到对应的运算:
\(1 xor 1=0,1xor0=1,0xor0=0\)
\(1\&1=1,1\&0=0,0\&0=0\)
那么我们就可以将读进来的数字按照奇偶数分类即可
官方题解给的讲解挺细,我给一个不太一样的思路(正确性不清楚,但反正过了):
显然,当所有数字都是偶数时,牛妹必胜
那么很显然,牛牛必须尽可能的减少偶数,牛妹必须尽可能的增加偶数
那么只需要一个 \(for\) 循环模拟下这个过程就好了,复杂度可以接受
#include<bits/stdc++.h>
using namespace std;
int n, x, y;
int main()
{
scanf("%d", &n);
for (int i = 1, tmp; i <= n; ++i) {
scanf("%d", &tmp);
tmp % 2 ? x++ : y++;
}
int user = 1;
for (; n > 1; --n, user = 1 - user) {
if (user == 1) {
if(y)y--;else x--;
}
else {
if (x > 1) x -= 2, y += 1;
else if(x == 1) x--;
else y-=1;
}
}
if (x) puts("NiuNiu");else puts("NiuMei");
return 0;
}
当然,如果复杂度接受不了呢?
其实也很简单,基于上面的推理,我们可以继续推,得到一个推论:当偶数数量大于等于两个的时候,牛妹必胜(因为此时可以保证两人僵持的情况下,奇数不断消耗,偶数数量不变,一直拖到全部是偶数的情况,详情建议看官方题解)