【解题报告】牛客暑假多校 Round 1


比赛链接: https://ac.nowcoder.com/acm/contest/11166

还在补题,但是好多,补不完了qwq。

已完成:A (B) D F I J

A - Alice and Bob

题意简介

两堆石子,分别是 \(n\)\(m\) 个,Alice 先取,Bob 后取。每次,可以从其中一堆中取走 \(k\) ( \(k>0\) ) 个,然后从另一堆中取走 \(s\times k\) ( \(s\geq 0\) ) 。轮到某个人时石子都没了,那个人就会输掉。现在给你 10000 组询问,\(1\leq n,m\leq 5\times 10^3\),要你输出每组询问谁会赢。

思路分析

想不出正解,但是先暴力算一遍再说。记 \(f(i,j)\) 表示第一堆石子剩下 \(i\) ,第二堆石子剩下 \(j\) 的情况下先手方的状态,\(1\) 表示必胜 ,有:\(f(i,j) = \max{(1-f(i-k,j-ks))}\)

因为发现必败态只有 2719 种,于是存下来,每次询问直接二分查找。

上面是比赛时用的打表思路。

经过后续的听讲,我们知道这道题可以利用每个 i 只唯一对应一个必败态来搞事,而且据出题人说可以跑得飞快,于是我就自己敲了一个。

思路就是用 \(Q_i\) 存下对于每个 \(i\) ,与之唯一对应的必败态 \(j\)(当然也可能不存在,记为 \(-1\))。

然后再根据这些必败态,枚举倍数来排除必胜态,找到下一个必败态。

理论复杂度是 \(O(n^2 \log n)\) 的,但因为减了不少枝,所以跑得飞快,500 来毫秒左右。

但是我并没有理解出题人说的 \(n = 10000\) 也可以跑出来需要怎么做,因为我无法省去这个二维数组,求大佬教教我qwq。

解题代码

#include 
#include 
#include 
#include 
using namespace std;
const int N = 5005;
int n, m, Q[N]; bool vis[N][N];
int main() {
	memset(Q, -1, sizeof(Q));
	Q[0] = 0;
	for(int i=0; i<=5000; i++) {
		if(Q[i] != -1) {
			for(int j=Q[i]+1; j<=5000; j++) {
				for(int k=i; k<=5000; k+=j-Q[i]) vis[k][j] = 1;
			}
			continue;
		}
		for(int j=i-1; j>=0; j--) {
			if(Q[j] == -1) continue;
			for(int k=Q[j]; k<=5000; k+=i-j) vis[i][k] = 1;
		}
		for(int j=i+1; j<=5000; j++) {
			if(!vis[i][j]) {
				Q[i] = j, Q[j] = i;
				break;
			}
		}
		if(Q[i] != -1) {
			for(int j=Q[i]+1; j<=5000; j++) {
				for(int k=i+j-Q[i]; k<=5000; k+=j-Q[i]) vis[k][j] = 1;
			}
		}
	}
	int t; scanf("%d", &t);
	while(t--) {
		scanf("%d%d", &n, &m);
		if(Q[n] != -1 && Q[n] == m) puts("Bob");
		else puts("Alice");
	}
	return 0;
}

B - Ball Dropping

题意简介

如上图,给你 \(a,b,r,h\) ,问你球是否会从漏斗里掉出去。如果不会,求图上红边的长度。

数据保证 \(2r \neq b\) , \(2r , \(2r

思路分析

队友切的。几何题一生之敌qwq。

TODO

通过代码见:提交记录

C - Cut the Tree

题意简介

TODO

思路分析

TODO

D - Determine the Photo Position

题意简介

给你一个只含 0 和 1 的 \(n\times n\) 的矩阵,表示相片上有的地方站了学生有的地方没站。现在告诉你有 \(m\) 个老师站成一排,要求你把这一排老师完整地、不能拆分或旋转地,插入到相片地空白处。问你有多少种插入方式。

思路分析

签到。求出所有的连续的 0 段的 0 的数目 c,每个连续 0 段对答案的贡献是 \(\max{(c-m+1, 0)}\)

解题代码

#include 
#include 
#include 
using namespace std;
const int N=2005;
int n,m; char s[N][N],s2[N];
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf(" %s",s[i]+1);
    scanf(" %s",s2+1);
    int ans=0;
    for(int i=1;i<=n;i++){
        int c0=0;
        for(int j=1;j<=n+1;j++){
            if(s[i][j]!=s[i][j-1]){
                if(s[i][j]!='0'){
                    ans+=max(0,c0-m+1);
                    c0=0;
                }else{
                    c0=1;
                }
            }else if(s[i][j]=='0') c0++;
        }
    }
    printf("%d",ans);
    return 0;
}

E - Escape along Water Pipe

题意简介

TODO

思路分析

TODO

F - Find 3-friendly Integers

题意简介

一个整数被称为 3友好整数 ,当且仅当可以从它的十进制表示中找到一个连续子串,这个连续子串的表示的数字是 3 的倍数。

给你 10000 组询问,问你 \(L\)\(R\) 里有多少个整数满足上述要求。( \(1\leq L\leq R \leq 10^{18}\) )

思路分析

我们知道,一个数是三的倍数,那么它的各位数之和也是三的倍数。

想要方便直观地各位数之和,那么我们可以给每一位数都模个 3 。

我们知道,想要一个数想要不满足题意,它的各个数字模 3 后首先不能是 0 。

所以,我们试着用数字 1 和 2 来构造不满足题意的数,不难发现,根本无法构造出 4 位数以上的数字。

所以,对于位数少的,我们直接预处理出那些数字满足题意,对于位数大的部分,它一定是满足题意的,直接 \(R-L+1\) 就完事了。

解题代码

#include 
#include 
using namespace std;
typedef long long ll;
const int N=1e6;
int vis[N];
inline void calc(int x){
    static int dig[1000];
    int cn=0;
    int ux=x;
    while(x){
        dig[++cn]=x%10;
        x/=10;
    }
    for(int i=1;i<=cn;i++) dig[i]+=dig[i-1];
    for(int i=1;i<=cn;i++) for(int j=i;j<=cn;j++){
        if((dig[j]-dig[i-1])%3==0){
            vis[ux] = 1; return;
        }
    }
    return;
}
int main(){
    int t; scanf("%d",&t);
    vis[0]=1;
    for(int i=1;i<=N;i++) {
        calc(i);
        vis[i]+=vis[i-1];
    }
    while(t--){
        ll L,R; scanf("%lld%lld",&L,&R);
        ll ans=0;
        if(R<=N) ans=vis[R]-vis[L-1];
        else if(L<=N) ans=R-N+vis[N]-vis[L-1];
        else ans=R-L+1;
        printf("%lld\n",ans);
    }
    return 0;
}

G - Game of Swapping Numbers

题意简介

TODO

思路分析

TODO

H - Hash Function

题意简介

给你一个大小为 \(n\) 的没有重复数字的集合 \(\{a_i\}\),问你使集合内所有数对 \(x\) 取模的答案两两不同的最小 \(x\)\(0\leq a_i\leq 500000\)\(1\leq n \leq 500000\)

思路分析

比赛时开桶暴力瞎搞的。数据加强了之后烂了。不会多项式,下一个。

TODO

I - Increasing Subsequence

题意简介

Alice 和 Bob 轮流对一个排列进行取数。每次取都要比上一个被取的数字更大。同时,对于同一个人,他取数时除了要满足比上一个数大外,还要求新取的数要在自己上一次取的数的右边。如果这一轮里一个人可以取的数不止一个,他会随机等概率拿一个出来。求游戏进行的轮次的期望值。

思路分析

比赛时有一个 O(n^3) 的思路,但是没时间了,细节也没搞清楚,于是没搞出来,赛后重新复盘了一下。

我们记 \(p(i, j)\) 为 Alice 选了 \(i\) 次,Bob 选了 \(j\) 次的概率。

显然的,\(A_i > A_j\) 时,当前这个状态最后一次取的人是 Alice(因为每次取数要比上一次取数更大),所以这个状态必然是由 \(p(k, j)\) 转移过来的。且根据题意,我们可以知道,这个状态必须满足 \(A_k < A_j\),和 \(k < i\)

而从 \((k ,j)\) 转移到 \((i, j)\) ,意味着 Alice 在 \(k\) 往后的位置中所有能选的位置里,恰好挑中了 \(i\)

于是,我们有转移:\(p(i, j) = \sum p(k, j) \times \frac{1}{c}\),其中 \(c\) 表示 \(k\) 往后的位置中能选的位置的数量,且 \(k\) 必须满足上面分析的条件(搞进公式里太复杂了,自已意会一下好了qwq)。

同理,对于 \(A_j > A_i\),我们有转移:\(p(i, j) = \sum p(i, k) \times \frac{1}{c}\)

有了这个东西之后,轮次期望的转移就变得非常明显了。

我们记 \(f(i, j)\) 为 Alice 选了 \(i\) 次,Bob 选了 \(j\) 次的轮次期望。

于是,对于 \(A_i > A_j\) 有:\(f(i, j) = \sum f(k, j) \times \frac{1}{c} + 1 \times p(k, j) \times \frac{1}{c}\)

小于的情况类似。

接下来考虑如何实现。

首先,题目 \(n \leq 5000\) ,因此我们只能是 \(O(n^2)\) ,甚至不能带 \(\log n\) 。发现需要求逆元的 \(c\) 小于 \(n\),所以我们先预处理出 \(1\)\(n\) 的所有逆元。

对于 \(c\) 的快速求解,我们可以开个二维数组,用 \(cnt(i, j)\) ,表示从下标 \(i\) 开始的后缀里,值小于等于 \(j\) 的数量(相当于对每个后缀开了一个桶)。

接下来,仔细观察转移的式子,我们可以发现,大于的状态必然从小于的状态转移过来,而且转移过程与 \(i\)\(j\))无关,所以我们可以维护类似前缀和的东西。

具体实现见代码。

解题代码

代码中的注释部分为 \(O(n^3)\) 的写法。

#include 
typedef long long ll;
const ll mod = 998244353, N = 5005;
int n; ll A[N], P[N], f[N][N], cnt[N][N], p[N][N], sp1[N], sf1[N], sp2[N][N], sf2[N][N], inv[N];
inline char getc() {
    static char buf[1<<14], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1<<14, stdin), p1 == p2) ? EOF : *p1++;
}
inline ll scan() {
    ll x = 0; char ch = 0;
    while(ch < 48) ch = getc();
    while(ch >= 48) x = x * 10 + ch - 48, ch = getc();
    return x;
}
inline ll _pow(ll x, ll p) {
    ll ans = 1;
    while(p) {
        if(p&1) ans = ans * x %mod;
        x = x * x % mod; p >>= 1;
    }
    return ans;
}
inline ll _inv(ll x) {
    return _pow(x, mod - 2);
}
inline ll add(ll a, ll b) {
    return a + b >= mod ? a + b - mod : a + b;
}
int main() {
    n = scan();
    for(int i=1; i<=n; i++) {
        A[i] = scan();
        P[A[i]] = i;
        inv[i] = _inv(i);
    }
    for(int i=1; i<=n; i++) {
        for(int j=i; j<=n; j++) {
            cnt[i][A[j]]++;
        }
        for(int j=1; j<=n; j++) {
            cnt[i][j] += cnt[i][j-1];
        }
    }
    p[0][0] = 1;
    for(int i=1; i<=n; i++) {
        p[i][0] = f[i][0] = inv[n];
        int _c = cnt[1][n] - cnt[1][A[i]]; 
        sp2[i][0] = p[i][0] * inv[_c] % mod;
        sf2[i][0] = (f[i][0] + 1 * p[i][0]) * inv[_c] % mod;
        for(int j=1; j<=n; j++) {
            // 计算 f[i][j] 和 p[i][j]
            if(A[i] > A[j]) {
                /*for(int k=1; k A[j]) {
            int c = cnt[j+1][n] - cnt[j+1][A[i]];
            if(c == 0) res = add(res, f[i][j]);
        }
        if(A[j] > A[i]) {
            int c = cnt[i+1][n] - cnt[i+1][A[j]];
            if(c == 0) res = add(res, f[i][j]);
        }
    }
    printf("%lld", res);
    return 0;
}

J - Journey among Railway Stations

题意简介

\(n\) 个车站,每个车站有一个开启时间和一个关闭时间。如果提早到了,需要等到开启时间才能走,如果晚到了,就再也过不去了。

相邻车站有一条从左到右的单向道路,表示需要走多少时间才能从一个车站到另一个车站。

现在有两种操作,一种询问:

  1. 修改车站的起始时间。

  2. 修改车站间路的长度。

  3. 询问能否从第 \(l\) 个车站移动到第 \(r\) 个车站并通过。

思路分析

看一眼数据范围:\(n \leq 10^6\) 。还有修改和查询。是数据结构哒!

然后我就溜了。

补题的时候听了下讲解。这道题是个很巧妙的线段树题。

我们可以观察到,不管是多长的区间,我们画出进入区间的时间与离开区间的最早时间的函数,你总能发现这样的三部分:

  1. 早来了,在移动过程中需要经过等待,最终出区间的时间一样。

  2. 中间不需要任何等待,出区间的时间随入区间的时间单调递增,且斜率为 1。

  3. 来晚了,过不去了。

不难发现,我们只要维护三个值,就可以推出完整的函数图像:

  1. wait: 在这个时间之前来,一定需要等待

  2. late: 在这个时间之后来,一定过不去

  3. time: 从 wait 这个时间前来,会在什么时候离开(也就是可能的最早离开时间)。

具体的维护和初始化,画画图,仔细想想、分类讨论一下可以搞出来(语文能力差,表达不出来)。可以参考代码的 merge 函数,可能写得有点丑qwq。

因为都是单点修改,所以每次改完后递归到底,往上一路 maintain 就完事了。

解题代码

#include 
#include 
using namespace std;
typedef long long ll;
const int N = 1e6 + 5;
#define ls (u<<1)
#define rs ((u<<1)+1)
struct Node {
	ll wait, late, time;
	Node(): wait(0), late(0), time(0) {}
}Tr[N<<2];
int s[N], e[N], cost[N], n, q;
inline void debug(Node x) {
	printf("Wait: %lld, late: %lld, time: %lld\n", x.wait, x.late, x.time);
}
inline void output(int u, int l, int r) {
	if(l > r) return;
	printf("For [%d, %d]: ", l, r); 
	debug(Tr[u]);
	if(l == r) return;
	int mid = (l + r) >> 1;
	output(ls, l, mid); 
	output(rs, mid + 1, r);
}
inline Node merge(Node A, Node B, ll w) {
	Node C = Node();
	if(A.time == -1 || B.time == -1) return C.time = -1, C;
	ll ti = A.time + w;
	if(ti > B.late) return C.time = -1, C;
	if(ti >= B.wait) C.wait = A.wait;
	else {
		C.wait = A.wait + (B.wait - ti);
		ti = B.wait;
	}
	C.time = B.time + (ti - B.wait);
	C.late = min(A.late, C.wait + (B.late - ti));
	return C;
}
inline void maintain(int u, int l, int r) {
	int mid = (l + r) >> 1;
	Tr[u] = merge(Tr[ls], Tr[rs], cost[mid]);
}
void build(int u, int l, int r) {
	if(l == r) {
		Tr[u].wait = s[l]; 
		Tr[u].late = e[l];
		Tr[u].time = s[l];
		return;
	}
	int mid = (l + r) >> 1;
	build(ls, l, mid), build(rs, mid + 1, r);
	return maintain(u, l, r);
}
Node query(int u, int l, int r, int ql, int qr) {
	if(l > qr || r < ql) return Node();
	if(ql <= l && r <= qr) return Tr[u];
	int mid = (l + r) >> 1;
	if(l > qr || mid < ql) return query(rs, mid + 1, r, ql, qr);
	if(mid + 1 > qr || r < ql) return query(ls, l, mid, ql, qr);
	return merge(query(ls, l, mid, ql, qr), query(rs, mid + 1, r, ql, qr), cost[mid]);
}
void update(int u, int l, int r, int x) {
	if(l > x || r < x) return;
	if(l == r) {
		Tr[u].wait = s[l];
		Tr[u].late = e[l];
		Tr[u].time = s[l];
		return;
	}
	int mid = (l + r) >> 1;
	update(ls, l, mid, x); update(rs, mid + 1, r, x);
	return maintain(u, l, r);
}
int main() {
	int T; scanf("%d", &T);
	while(T--) {
		scanf("%d", &n);
		for(int i=1; i<=n; i++) scanf("%d", s+i);
		for(int i=1; i<=n; i++) scanf("%d", e+i);
		for(int i=1; i

K - Knowledge Test about Match

题意简介

TODO

思路分析

TODO