比赛链接:https://ac.nowcoder.com/acm/contest/11166
菜狗,大佬勿喷
简单题解
- A. Alice and Bob | 博弈论 | sg函数
- B. Ball Dropping | 简单计算几何
- C. Cut the Tree | 线段树 |未补
- D. Determine the Photo Position | 签到
- E. Escape along Water Pipe | BFS和DFS
- F. Find 3-friendly Integers | 抽屉原理
- G. Game of Swapping Numbers | 思维、对绝对值的理解
- H. Hash Function | 数学、FFT
- I. Increasing Subsequence | 期望DP | 未补
- J. Journey among Railway Stations | 线段树
- K. Knowledge Test about Match | 乱搞
- 总结
A. Alice and Bob | 博弈论 | sg函数
题目大意:
两人博弈。有两堆石头,每次一个人从一堆中拿 k 个,同时从另一堆拿 k * s(s >= 0) 个,问谁先不能拿。
数据均不大于 5000。
思路:
规定:一堆石头有 n 个,另一对石头有 m 个
- 第一点:可以证明,对于 n 的每一个值,只有一个对应的 mx 使得(n,mx)构成必败态
证明:如果(n, m1) 和 (n, m2 ) 都是必败态,先手第一次在第二堆取 | m1 - m2 | 个石头的话,后手会面对(n, m2 ) 的局面。这时发生冲突,(n, m1) 的必败态就不成立。 - 第二点:显然 (0,0) 是必败态,O(N2) 处理出所有局面的,O( 1 ) 查询即可
- 补充:本题显然,能到达必败态的局面都是必胜态
AC代码:
#include <bits/stdc++.h>
#define ll long long
const int maxn = 1e5 + 5;
using namespace std;
bool sg[5001][5001];
int main(){
for(int i = 0; i <= 5000; i++){
for(int j = 0; j <= 5000; j++){
if(sg[i][j] == 0){ // 拿一次就可以到达必败态的局面就是必胜态
for(int k = 1; i+k <= 5000; k++){
for(int s = 0; j+s*k <= 5000; s++){
sg[i+k][j+s*k] = 1;
}
}
for(int k = 1; j+k <= 5000; k++){
for(int s = 0; i+s*k <= 5000; s++){
sg[i+s*k][j+k] = 1;
}
}
}
}
}
int ncase;
scanf("%d", &ncase);
while(ncase--){
int n, m;
scanf("%d %d", &n, &m);
if(sg[n][m]) cout << "Alice" << endl;
else cout << "Bob" << endl;
}
return 0;
}
B. Ball Dropping | 简单计算几何
题目大意:
给出 a 、b、h 和 r ,求标红线段的长度,如果小球会掉下去,输出 Drop。
思路:
初中数学题,相似三角形比一下就出来了。
注意一种情况,如图
AC代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int main(){
double r, a, b, h;
scanf("%lf %lf %lf %lf", &r, &a, &b, &h);
if(2.0 * r < b){
printf("Drop\n");
}else{
double x = 1.0 * (b * h) / (a - b);
double y = sqrt((x * x) + b * b / 4.0);
double z = 2.0 * r * y / b;
double res = z - x;
printf("Stuck\n");
printf("%.10lf\n", res);
}
return 0;
}
C. Cut the Tree | 线段树 |未补
是一个很难的线段树,未补
D. Determine the Photo Position | 签到
题目大意:
给出 n * n 的 01 矩阵,用 1*m 的矩阵覆盖一段 0(不能旋转),输出可行的方案数
思路:
每一行处理一个前缀和,计算 0 的个数,然后查询即可
AC代码:
#include <vector>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#include <set>
#include <stack>
#define ll long long
#define chushi(a, b) memset(a, b, sizeof(a))
#define endl "\n"
const double eps = 1e-8;
const ll INF=0x3f3f3f3f;
const int mod=998244353;
const int maxn = 1e5 + 5;
using namespace std;
int ma[2005][2005];
char s[2005];
int main(){
int n, m;
cin >> n >> m;
char c;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
cin >> c;
if(c == '1') ma[i][j] = 0;
else ma[i][j] = 1;
}
}
cin >> s;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
ma[i][j] += ma[i][j-1];
}
}
int res = 0;
for(int i = 1; i <= n; i++){
for(int j = m; j <= n; j++){
if(ma[i][j] - ma[i][j-m] == m) res++;
}
}
cout << res << endl;
return 0;
}
E. Escape along Water Pipe | BFS和DFS
题目大意:
给出一个 n * m 的水管图,要从 (1,1) 顶部走到 (n,m) 底部。每走一步前,可以选择一个管道集合旋转相同的角度(0,90,180,270)。要求在 20nm 步前走到终点或者输出无解。
思路:
麻烦的搜索题,/(ㄒoㄒ)/~~
- 第一点:先 BFS ,看是否存在一个合法的路径。在 BFS 时,可以先不考虑水管如何旋转,用 (x, y, d) 表示一个状态,即从 d 方向进入(x, y)。同时,记录一下是由那个状态进入到 (x, y, d) 中,递归路径用。
- 第二点:本题是默认由 (0, 1) 进入(1, 1), 并且要求能从 (n,m) 到 (n+1,m)。看图就会明白:
- 第三点:由 (x, y, d) 和 pre[(x, y, d)] 可以知道进入(x, y) 的上一个管道的形状 (进入管道方向和离管道的方向就决定了管道的摆放方式)
- 第四点:如果有达到(n+1, m, 0)状态的,即存在一种方案,DFS找路径即可
- 第五点:一个管道会被从不同时候进入多次,故而,这个管道可能被旋转多次,敲黑板!!!
AC代码:
#include <stdio.h>
#include <vector>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#include <set>
#include <stack>
#define ll long long
#define chushi(a, b) memset(a, b, sizeof(a))
#define endl "\n"
const double eps = 1e-8;
const ll INF=0x3f3f3f3f;
const int mod=1e9 + 7;
const int maxn = 1e6 + 5;
const int N=1005;
using namespace std;
typedef struct Node{
int x;
int y;
int d;
} node;
int ma[1005][1005];
bool vis[1005][1005][4]; // 已经到达过这个状态
node pre[1005][1005][4]; // 到达这个状态上一个状态的信息
int n, m;
queue<node> qu;
// 0 1 2 3
// ↓ ← ↑ →
bool check(int x, int y, int d){
if(x == n+1 && y == m && d == 0) return true;
if(x < 1 || x > n || y < 1 || y > m) return false;
if(vis[x][y][d]) return false;
return true;
}
void push(int x, int y, int d, int xx, int yy, int dd){
pre[xx][yy][dd].x = x; pre[xx][yy][dd].y = y; pre[xx][yy][dd].d = d;
qu.push({xx, yy, dd});
}
void bfs(){
qu.push({1, 1, 0});
while(!qu.empty()){
int x = qu.front().x, y = qu.front().y, d = qu.front().d;
qu.pop();
if(vis[x][y][d]) continue;
vis[x][y][d] = 1;
if(x == n+1 && y == m && d == 0) break;
if(ma[x][y] > 3){
if(d == 0 && check(x+1, y, d)) push(x, y, d, x+1, y, d);
else if(d == 1 && check(x, y-1, d)) push(x, y, d, x, y-1, d);
else if(d == 2 && check(x-1, y, d)) push(x, y, d, x-1, y, d);
else if(d == 3 && check(x, y+1, d)) push(x, y, d, x, y+1, d);
}
else{
if(d == 0 || d == 2){
if(check(x, y-1, 1)) push(x, y, d, x, y-1, 1);
if(check(x, y+1, 3)) push(x, y, d, x, y+1, 3);
}
else{
if(check(x+1, y, 0)) push(x, y, d, x+1, y, 0);
if(check(x-1, y, 2)) push(x, y, d, x-1, y, 2);
}
}
}
while(!qu.empty()) qu.pop();
}
int get_id(int x, int y, int d){
int dd = pre[x][y][d].d;
if(dd == d) return d % 2 == 0 ? 5 : 4;
if(dd == 0) return d == 1 ? 0 : 1;
if(dd == 1) return d == 2 ? 1 : 2;
if(dd == 2) return d == 3 ? 2 : 3;
if(dd == 3) return d == 2 ? 0 : 3;
}
void dfs(int x, int y, int d, int step){
if(x == 1 && y == 1 && d == 0){
cout << "YES" << endl;
cout << step*2 << endl;
return;
}
dfs(pre[x][y][d].x, pre[x][y][d].y, pre[x][y][d].d, step+1);
int n_id = get_id(x, y, d);
int id = ma[pre[x][y][d].x][pre[x][y][d].y];
cout << "1 " << (n_id - id + 4) % 4 * 90 << " " << pre[x][y][d].x << " " << pre[x][y][d].y << endl;
ma[pre[x][y][d].x][pre[x][y][d].y] = n_id; // 注意修改管道的状态,会多次进入
cout << "0 " << pre[x][y][d].x << " " << pre[x][y][d].y << endl;
}
int main() {
int ncase;
cin >> ncase;
while(ncase--){
cin >> n >> m;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
cin >> ma[i][j];
}
}
bfs();
if(vis[n+1][m][0]) dfs(n+1, m, 0, 0);
else cout << "NO" << endl;
for(int i = 1; i <= n+1; i++){
for(int j = 1; j <= m; j++){
for(int k = 0; k < 4; k++){
vis[i][j][k] = 0;
}
}
}
}
return 0;
}
F. Find 3-friendly Integers | 抽屉原理
题目大意:
定义一个自然数是 3-friendly 的,如果它存在一个子串(允许前导0)是 3 的倍数。多组数据,求 (L, R) 中 3-friendly 的数的个数(闭区间)。
数据范围:1e18
思路:
在 0 到 9 中 mod 3 的结果只有 0、1 和 2。根据抽屉原理,三位数的以后的所有数字都是 3-friendly。
证明:
- 数位中存在 mod 3 = 0 的数直接就是 3-friendly 数字;
- 如果不存在 mod 3 = 0 的数字,可能情况有:111,222,112,121,221,212。可以发现,全部满足 3-friendly的定义。
其他数字暴力即可
AC代码:
#include <vector>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <map>
#include <set>
#include <stack>
#define ll long long
#define chushi(a, b) memset(a, b, sizeof(a))
#define endl "\n"
const double eps = 1e-8;
const ll INF=0x3f3f3f3f;
const int mod=998244353;
const int maxn = 1e5 + 5;
using namespace std;
ll f(ll x){
if(x == 0) return 0;
ll res = 0;
for(int i = 1; i <= min(x, 99ll); i++){
if(i % 3 == 0) res++;
else if(i%10%3 == 0) res++;
else if(i > 9 && i/10%3 == 0) res++;
}
if(x > 99) res += x - 99;
return res;
}
int main(){
int ncase;
cin >> ncase;
while(ncase--){
ll l, r;
cin >> l >> r;
ll res = f(r) - f(l-1);
cout << res << endl;
}
return 0;
}
G. Game of Swapping Numbers | 思维、对绝对值的理解
题目大意:
给定序列 A,B,需要交换恰好 k 次 A 中两个不同的数,使得 A,B 每个位置的绝对差值和最大。
数据范围:N <= 100000
思路:
我是没太理解讲解人给出的方法,想了另外的方法解决这个题
- 第一点:在一维的数轴上,| a - b | 表示线段ab的长度
- 第二点:可以把 | ai - bi | 当作线段 aibi 的长度,这个题就转化为,可以交换一些线段的端点,让所有的线段长度和大
- 第三点:没有相交的两个线段,交换点之后,长度一定变长,如图
- 第四点:显然,增加的线段长度为:2 * ( min(a2, b2) - max(a1, b1) )
- 第五点:根据贪心,取前 k 大的 2 * ( min(ax, bx) - max(ay, by) ) 即可
- 第六点:如果没有相交的线段不足 k 个,我们随意交换其他的线段,把多的交换次数浪费掉就好,保持最优
AC代码:
#include <bits/stdc++.h>
#define ll long long
const int maxn = 1e6 + 5;
using namespace std;
int a[maxn];
int b[maxn];
int main(){
int n, k;
cin >> n >> k;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++) cin >> b[i];
// 让 a 中为 min(a, b),b 中为 max(a, b)
for(int i = 1; i <= n; i++) if(a[i] > b[i]) swap(a[i], b[i]);
// 计算出所有线段长度
ll sum = 0;
for(int i = 1; i <= n; i++) sum += b[i] - a[i];
// 排序
sort(a+1, a+1+n);
sort(b+1, b+1+n);
// 取前 k 长
for(int i = 1; i <= k && i <= n; i++){
if(a[n+1-i] > b[i]){
sum += 2 * (a[n+1-i] - b[i]);
}
}
cout << sum << endl;
return 0;
}
H. Hash Function | 数学、FFT
题目大意:
给定 n 个互不相同的数,找一个最小的模域,使得它们在这个模域下互不相同。
数据范围:所有数小于 500000,保证 a[i] != a[j]
思路:
这个题是队友补的,FFT 我不会,/(ㄒoㄒ)/~~
- 第一点:如果 a % mod != b % mod,则 | a - b | % mod != 0
- 第二点:问题转化,计算任意两个数差值,找到不能被这些差整除的第一个数就是答案
- 第三点:任意两个数差值,朴素做法是 O(N2),需要用 FFT / NTT 加速到 O(logn * n)
AC代码:
#include <cstdio>
#include <cmath>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_map>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 2e6+10;
const double PI = acos(-1);
int p[maxn], ans[maxn];
struct Complex{
double x, y;
Complex (double x = 0, double y = 0) : x(x), y(y) { }
}a[maxn], b[maxn];
Complex operator * (Complex J, Complex Q) {
//模长相乘,幅度相加
return Complex(J.x * Q.x - J.y * Q.y, J.x * Q.y + J.y * Q.x);
}
Complex operator - (Complex J, Complex Q) {
return Complex(J.x - Q.x, J.y - Q.y);
}
Complex operator + (Complex J, Complex Q) {
return Complex(J.x + Q.x, J.y + Q.y);
}
int R[maxn];
int bit, tot;
//FFT板子
void FFT(Complex *A, int type){
//按照R数组对A数组进行实际排序
for(int i = 0; i < tot; ++ i)
if(i < R[i])
swap(A[i], A[R[i]]);//if保证只换一次
for(int mid = 1; mid < tot; mid <<= 1) {
Complex wn({cos(PI / mid), type * sin(PI / mid)});
for(int pos = 0; pos < tot; pos += mid*2) {
Complex w(1, 0);
for(int k = 0; k < mid; ++ k, w = w * wn) {
Complex x = A[pos + k];
Complex y = w * A[pos + mid + k];
A[pos + k] = x + y;
A[pos + mid + k] = x - y;
}
}
}
}
int main(){
int n;
scanf("%d",&n);
int maxx = 0;
for(int i=0; i<n; i++){
scanf("%d",&p[i]);
maxx = max(maxx, p[i]);
}
for(int i=0; i<n; i++){
a[p[i]].x = 1;
b[maxx - p[i]].x = 1;
}
while((1<<bit) <= 2*maxx) bit++;
tot = 1<<bit;
for(int i=0; i<tot; i++){
R[i] = (R[i >> 1] >> 1) | ((i & 1) << (bit - 1));
}
FFT(a, 1); FFT(b, 1);
for(int i = 0; i < tot; ++i)
a[i] = a[i] * b[i];
FFT(a, -1);
for(int i=1; i<=maxx; i++){
ans[i] = (int)(a[i+maxx].x / tot + 0.5);
}
for(int i=1; i<= maxx+1; i++){
int f = 1;
for(int j=i; j<=maxx; j+=i){
if(ans[j]){
f = 0;
}
}
if(f){
printf("%d\n", i);
break;
}
}
return 0;
}
I. Increasing Subsequence | 期望DP | 未补
J. Journey among Railway Stations | 线段树
题目大意:
一段路上有 N 个点,每个点有一个合法时间段 [ui, vi],相邻两个点有一个长度。每次问,从任意时刻出发,是否可以以此到达 [ l, r ] 之间的所有点,使得到达时间满足每个点的合法区间(如果提前到可以等待,迟到了就失败)。同时还可能修改一段路的长度,或者修改一个点的合法时间段。
思路:
用线段树维护信息即可,每一个叶子节点 x 表示:第 x 个点是否合法,以及它的合法区间。
则对应的区间节点 (l, r) 表示:通过区间(l, r) 是否合法,以及它的合法区间
具体细节就看代码吧
AC代码:
代码链接,无语子,放线段树的代码说与其他文字大量重复
https://pasteme.cn/139596
K. Knowledge Test about Match | 乱搞
题目大意:
随机生成一个权值范围为 0 ~ n-1 的序列,你要用 0 ~ n-1 去和它匹配,匹配函数是 sqrt。
要求平均情况下和标准值偏差不能超过 4%。
思路:
乱搞的,出题人都说是乱搞的
AC代码:
#include <bits/stdc++.h>
#define ll long long
const int maxn = 1e5 + 5;
using namespace std;
int a[maxn];
double s[maxn];
int main(){
for(int i = 1; i < maxn; i++) s[i] = sqrt(1.0*i);
int ncase;
cin >> ncase;
while(ncase--){
int n;
cin >> n;
for(int i = 0; i < n; i++) cin >> a[i];
for(int i = 1; i <= 5; i++){
for(int j = 0; j < n; j++){
for(int k = j+1; k < n; k++){
if(s[abs(j-a[j])] + s[abs(k-a[k])] > s[abs(j-a[k])] + s[abs(k-a[j])]){
swap(a[j], a[k]);
}
}
}
}
for(int i = 0; i < n; i++) cout << a[i] << " ";
cout << endl;
}
return 0;
}
总结
A题:博弈论优先考虑四大基础博弈,再者是 SG函数,再往后就是纯思维博弈
D题:区间查询问题:前后缀、差分、二分、双指针尺取、线段树 或 树状数组、莫队 或 分块
E题:这个题和之前写过的题最大的区别在于:一个点会有很多状态,之前写过的题一个点一般都只有一个状态。直接 wa 成傻子,┭┮﹏┭┮
F题:1e18 的数据范围,必有公式或结论
G题:对于绝对值的理解,考虑它的几何意义。出题人题除的符号分配也是很好的模型。
H题:a 和 b 模 m 结果相同,有 | a - b | % m = 0。
J题:线段树初成长~~