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中两个不同位置的元素,最小化
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想的是卷积的意义 直接秒了这题 但是调试花了不少时间
考虑会冲突的情况,仅当
时会冲突。
那么最小的模数取\(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]\),距离游戏结束还需要进行的回合数的期望.
那么
其中\(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;
}