YjmStr

2021牛客暑期多校训练营1

场上过ABDFH,然后就全员摸鱼去了,,

A. Alice and Bob

两堆石头,分别有\(n\)\(m\)个。每次可以从其中一堆拿\(k(k>0)\)个,从另外一堆拿\(s\times k(s\ge 0)\)个,不能操作者输。二者均采取最优策略,每次询问给出\(n,m\),输出谁赢。

\(T(T\le1e4)\)组数据,\(1\le n,m\le5e3\).

前置知识:有向图游戏,\(SG\)函数

解:​

\((0,0)\)是必败态。 能走到它的全是必胜态

枚举\(i,j\),若\((i,j)\)能走到的所有状态中有一个必败态,那么\((i,j)\)为必胜态,否则\((i,j)\)为必败态。那么我们可以用所有的必败态来转移。

不难发现一个性质:对于某一个\(i\),只存在唯一的一个\(j\)使其成为必败态。

证:若\((i,j_1)\)\((i,j_2)\)均为必败态,从其中一个状态可以一步走到另外一个状态,与必败态的定义矛盾。

同理对于某一个\(j\),也只存在唯一一个\(i\)使其成为必败态。

那么必败态只有5000个,暴力算一下,后面那俩个循环是n*调和级数的复杂度,复杂度为\(O(n^2+n^2\ln n)\)

#include<bits/stdc++.h>
using namespace std;
const int maxn = 5007;
bool book[maxn][maxn];
int T, n, m;
int main() {
    for (int i = 0; i <= 5000; i++) {
        for (int j = 0; j <= 5000; j++) {
            if (!book[i][j]) {
                for (int k = 1; i + k <= 5000; k++) {
                    for (int s = 0; j + s * k <= 5000; s++) {
                        book[i+k][s*k+j] = 1;
                    }
                }
                for (int k = 1; j + k <= 5000; j++) {
                    for (int s = 0; s * k + i <= 5000; s++) {
                        book[i+s*k][j+k] = 1;
                    }
                }
            }
        }
    }
    cin >> T;
    while (T--) {
        cin >> n >> m;
        puts(book[n][m] ? "Alice" : "Bob");
    }
    return 0;
}

B.

签到题 略

D.

签到题 略

F.

签到题 要用到鸽巢原理 这样只需要暴力求小于1k的答案即可。

G. Game of Swapping Numbers

给定两个长为N的数组,执行恰好k次操作,每次操作交换A中两个不同位置的元素,最小化

\[\sum_{i=1}^{n}|A_i-B_i| \]

sol:网上看的题解

看不懂官方题解说的什么玩意 这个讲的好啊!绝对值是数轴上两点间的距离,题目就变成了对若干个线段的端点进行交换,使得长度总和最大。

交换不相交的线段的端点可以使得答案变优,无论a是左端点还是右端点,每次这样的交换对答案的贡献为\(2\times(min(a_2,b_2)-max(a_1,b_1))\),并且还能发现,交换相交线段的aa或是ab或是bb对答案的贡献都是一样的,所以可以直接钦定左端点为a。

贪心地取前\(k\)大的贡献即可。

把线段的左端点和右端点分别排序,让前\(k\)小的左端点和前\(k\)大的右端点组成线段,\(O(n\log n)\)搞一下取前\(k\)大的贡献。相交的线段不足\(k\)对的话,一定能找到一组\(i,j\),使得交换之后他们对答案的贡献不变,对着他们浪费掉剩下的次数就行。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 7;
#define ll long long
int a[maxn], b[maxn], n, k;
ll ans;
int rd() {
    int s = 0, f = 1; char c = getchar();
    while (c < \'0\' || c > \'9\') {
        if (c == \'-\') f = -1;
        c = getchar();
    }
    while (c >= \'0\' && c <= \'9\') {
        s = s * 10 + c - \'0\';
        c = getchar();
    }
    return s * f;
}
int main() {
    n = rd(), k = rd();
    for (int i = 1; i <= n; i++) a[i] = rd();
    for (int i = 1; i <= n; i++) b[i] = rd(), ans += abs(b[i]-a[i]);
    for (int i = 1 ; i <= n; i++) 
        if (a[i] > b[i]) 
            a[i] ^= b[i] ^= a[i] ^= b[i];
    sort(a + 1, a + n + 1);
    sort(b + 1, b + n + 1);
    for (int i = 1; i <= min(k, n); i++) {
        if (a[n-i+1]>b[i]) ans += 2*(a[n-i+1]-b[i]);
    }
    cout << ans << endl;
    return 0;
}

H. Hash Function

给一个\(n\)个元素的集合,集合中的元素值满足\(0\le x \le500000\),求出一个最小的模数,使得集合中每个元素模这个数之后没有重复。

看了眼牛客题交 全是暴力过的,跑得比正解还快

场上jyz想的是卷积的意义 直接秒了这题 但是调试花了不少时间

考虑会冲突的情况,仅当

\[a_i-a_j\equiv 0 \mod seed \]

时会冲突。

那么最小的模数取\(mex\{\forall i,j, (a_i-a_j)的因子\}\)即可

用fft/ntt求出任意两个数之间的差值,搞两个次数界为\(maxv\)的多项式,对于每个\(a[i]\),让其中一个多项式第\(a[i]\)项为1,另外一个多项式的第\(maxv-a[i]\)项系数为1,这样所有\(maxv+a[i]-a[j]\)项的次方项对应的系数就都是1.

之后模数从\(n\)开始枚举到\(maxv\),判断模数及其倍数是否是\((a[i]-a[j])\),找第一个合法的就行

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const double PI = acos(-1.0);
const int maxn = 10 + (1 << 20), maxv = 500007;
int Bit, Lim;
struct Complex {
	double x, y;
	Complex() {x = y = 0;}
	Complex(double _x, double _y) {
		x = _x, y = _y;
	}
}a[maxn], b[maxn];
Complex operator + (Complex a, Complex b) {
	return (Complex) {a.x + b.x, a.y + b.y};
}
Complex operator - (Complex a, Complex b) {
	return (Complex) {a.x - b.x, a.y - b.y};
}
Complex operator * (Complex a, Complex b) {
	return (Complex) {a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x};
}
Complex Conj(Complex a) {
	return (Complex) {a.x, -a.y};
}
void fft(Complex *a, int n, int f) {
	for (int i = 0, j = 0; i < n; i++) {
		if (i > j) swap(a[i], a[j]);
		for (int l = n >> 1; (j ^= l) < l; l >>= 1);
	}
	for (int i = 2; i <= n; i <<= 1) {//区间长度从小到大
		int m = i >> 1;
		Complex wn = (Complex) {cos(2 * PI / i), sin(2 * PI * f / i)};
		for (int k = 0; k < n; k += i) {	
			Complex t, w, u;
			w = (Complex) {1, 0};
			for (int j = 0; j < m; j++) {
				t = w * a[j + m + k];
				u = a[k + j];
				a[k + j] = u + t;
				a[k + j + m] = u - t;
				w = w * wn;
			}
		}
	}
	if (f == -1) {
		for (int i = 0; i < n; i++) {
			a[i].x /= n;
		}
	}
}
bool vis[maxn];
int rd() {
	int s = 0, f = 1; char c = getchar();
	while (c < \'0\' || c > \'9\') {
		if (c == \'-\') f = -1;
		c = getchar();
	}
	while (c >= \'0\' && c <= \'9\') {
		s = s * 10 + c - \'0\';
		c = getchar();
	}
	return s * f;
}
int main() {
	int n = rd();
	for (int i = 0; i < n; i++) {
		int x = rd();
		a[x].x = 1;
		b[maxv - x].x = 1;
	}
	Lim = 1, Bit = 0;
	while (Lim < 1000001) Lim <<= 1, Bit++;
	//cout << Lim << endl;
	fft(a, Lim, 1); fft(b, Lim, 1);
	for (int i = 0; i <= Lim; i++) {
		a[i] = a[i] * b[i];
	}
	fft(a, Lim, -1);
	for (int i = 0; i <= Lim; i++) {
		int tmp = (int) floor(a[i].x + 0.5);
		if (tmp > 0) {
			vis[abs(maxv-i)] = true;
		}
	}
	for (int i = n; i <= maxv; i++) {
		bool flag = 1;
		for (int j = i; j <= maxv; j += i) {
			if (vis[j]) {
				flag = 0;
				break;
			}
		}
		if (flag) {
			cout << i << endl;
			break;
		}
	}
	return 0;
}

I. Increasing Subsequence

A和B在长为n的排列上轮流进行操作,A先手,每一回合中,玩家选取的元素值需要比先前二人选出的所有元素值都大,除此之外,如果某人之前选择了下标为\(i\)的元素,那么这回合选出的元素要满足下标比\(i\)大,如果有多个元素可供选择,他将会等概率随机选取。不能操作时游戏结束,求期望回合数。

\(f[i][j]\)表示A上一轮选了\(a[i]\),B上一轮选择了\(a[j]\),距离游戏结束还需要进行的回合数的期望.

那么

\[f[i][j]=\left\{ \begin{array}{rcl} 1+inv[cntj[j]]\times\sum_{a[k]>a[j],k>i}f[k][j] & &{a[i]<a[j]}\\ 1+inv[cnti[i]]\times\sum_{a[k]>a[i],k>j}f[i][k] & &{a[i]>a[j]}\\ \end{array} \right. \]

其中\(cntj[j],cnti[i]\)分别表示“下标比当前的\(i\)大且元素值比当前的\(a[j]\)大的数的个数”,“下标比当前的\(j\)大且元素值比当前的\(a[i]\)大的数的个数”,通过倒着枚举i,j来保证下标符合要求

这个东西和后面和式里的东西都可以后缀和搞掉,这样就能\(O(1)\)转移了,复杂度\(O(n^2)\)

for (int i = n; i >= 0; i--) {
	for (int j = n; j >= 0; j--) {
        if (a[i] < a[j]) {
            cnti[i]++;	//下标比当前的j大且元素值比当前的a[i]大的数的个数
            f[i][j] = (sumj[j] * inv[cntj[j]] % md + 1) % md;
            //下标比当前的j大且元素值比当前的a[i]大的位置对答案的贡献
            sumi[i] = (sumi[i] + f[i][j]) % md;
        } else if (a[j] < a[i]) {
            cntj[j]++;
            f[i][j] = (sumi[i] * inv[cnti[i]] % md + 1) % md;
            sumj[j] = (sumj[j] + f[i][j]) % md;
        }
    }
}

最终的答案是所有\(f[i][0]*inv[n]\)的和

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn = 5007; 
const int md = 998244353;
ll a[maxn], inv[maxn], cnti[maxn], cntj[maxn], sumi[maxn], sumj[maxn], ans, f[maxn][maxn];
int rd() {
    int s = 0, f = 1; char c = getchar();
    while (c < \'0\' || c > \'9\') {if (c == \'-\') f = -1; c = getchar();}
    while (c >= \'0\' && c <= \'9\') {s = s * 10 + c - \'0\'; c = getchar();}
    return s * f;
}
int n;
int main() {
    n = rd();
    inv[1] = 1;
    for (int i = 2; i <= n; i++) {
        inv[i] = inv[md % i] * (md - md / i) % md;
    }
    
    for (int i = 1; i <= n; i++) a[i] = rd();
    for (int i = n; i >= 0; i--) {
        for (int j = n; j >= 0; j--) {
            if (a[i] < a[j]) {
                cnti[i]++;	//下标比当前的j大且元素值比当前的a[i]大的数的个数
                f[i][j] = (sumj[j] * inv[cntj[j]] % md + 1ll) % md;
                //下标比当前的j大且元素值比当前的a[i]大的位置对答案的贡献
                sumi[i] = (sumi[i] + f[i][j]) % md;
            } else if (a[j] < a[i]) {
                cntj[j]++;
                f[i][j] = (sumi[i] * inv[cnti[i]] % md + 1ll) % md;
                sumj[j] = (sumj[j] + f[i][j]) % md;
            }
        }
    }
    for (int i = 1; i <= n; i++) ans = (ans + f[i][0]) % md;
    cout << (ans * inv[n]) % md << endl;
    return 0;
}

分类:

技术点:

相关文章: