2021年暑假牛客多校第一场 | 总结


比赛链接:https://ac.nowcoder.com/acm/contest/11166
菜狗,大佬勿喷



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题:线段树初成长~~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值