2021牛客多校第一场
A. Alice and Bob
题意:给出两堆石子a、b,两名玩家轮流取石子,取法是在一堆石子里取k个,并在另一堆石子里取$s\times k$个$(s\ge 0)$,给定a、b,问先后手谁必胜
观察得到,对于一堆石子为i个,那么只有最多一个对应的j,使得两堆石子为i、j时为必败态,于是必败态总数不超过n。对于每一个必败态,先枚举k,再枚举s,得到所有能一步达到该状态的必胜态,复杂度为调和级数$O(n\log n)$
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long long LL;
const int N = 5e3 + 5;
int loss[N][N], win[N][N];
int vis[N], to[N];
int main() {
for (int i = 2; i <= 5000; i++) {
int j = i + 1;
for (; j <= 5000; j++) {
if (!win[i][j]) break;
}
if (j <= 5000) {
to[i] = j;
for (int k = 1; i + k <= 5000; k++) {
for (int p = 0; j + p <= 5000; p += k) {
int xx = i + k, yy = j + p;
if (xx > yy) swap(xx, yy);
win[xx][yy] = 1;
}
}
for (int k = 1; j + k <= 5000; k++) {
for (int p = 0; i + p <= 5000; p += k) {
int xx = i + p, yy = j + k;
if (xx > yy) swap(xx, yy);
win[xx][yy] = 1;
}
}
}
}
int T; scanf("%d", &T);
while(T--) {
int x, y; scanf("%d %d", &x, &y);
if (x > y) swap(x, y);
if (to[x] == y) printf("Bob\n");
else printf("Alice\n");
}
return 0;
}
G. Game of Swapping Numbers
H. Hash Function
题意:给定n个数$(0\le a_i\le 500000)$,求出最小的$seed$,使得每个数$a_i mod seed$结果不同
$seed$满足条件的充要条件是,对于任意两个数之差的绝对值$|a_i-a_j|,$$seed$都不是其因数
fft加速,求出某个数是否是任意两数之差,记录在一个数组里,然后从小到大枚举$seed$,若每个$k\times seed$都不存在,则将$seed$作为答案,复杂度类似于A题中的调和级数
#include <bits/stdc++.h>
#define fo(i, a, b) for (int i = a; i <= b; i++)
using namespace std;
typedef complex<double> cp;
const int N = 3e6 + 5;
const double pi = acos(-1);
cp a[N], b[N];
int n, m, limit, rev[N];
bool vis[N];
void fft(cp *a, int n, int inv)
{
int bit = 0;
while ((1 << bit) < n) bit++;
for (int i = 0; i < n; i++) {
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
if (i < rev[i]) swap(a[i], a[rev[i]]);//不加这条if会交换两次(就是没交换)
}
for (int mid = 1; mid < n; mid *= 2) {//mid是准备合并序列的长度的二分之一
cp temp(cos(pi / mid), inv * sin(pi / mid));//单位根,pi的系数2已经约掉了
for (int i=0;i<n;i+=mid*2)//mid*2是准备合并序列的长度,i是合并到了哪一位
{
cp omega(1,0);
for (int j=0;j<mid;j++,omega*=temp)//只扫左半部分,得到右半部分的答案
{
cp x=a[i+j],y=omega*a[i+j+mid];
a[i+j]=x+y,a[i+j+mid]=x-y;
}
}
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int x; scanf("%d", &x);
a[x + 500000].real(1);
b[-x + 500000].real(1);
}
limit = 1; while(limit < 1500000) limit <<= 1;
fft(a, limit, 1); fft(b, limit, 1);
for (int i = 0; i <= limit; i++) a[i] *= b[i];
fft(a, limit, -1);
for (int i = 500000; i <= 1500000; i++) {
int now = (int)(a[i].real() / limit + 0.5);
if (now) vis[abs(i - 1000000)] = 1;
}
for (int i = 1; i <= 1500001; i++) {
bool ok = 1;
for (int j = i; j <= 1500001; j += i) {
if (vis[j]) {
ok = 0;
break;
}
}
if (ok) {
printf("%d\n", i);
break;
}
}
return 0;
}
I. Increasing Subsequence
题意:给出排列P,两人轮流取数,要求取出的数比之前任何取的数都大,且在自己上次取的数的右边,若有多种取法则等概率选择,无人可取时终止,问期望轮数。
首先按照数值大小,从小到大排序,对应位置上的数为该数原来的位置。这样的话,在新的排列上考虑该问题,一个合法的选择方案一定是从左往右依次选择,且方案中奇数和偶数位上的数一定是分别递增的。
考虑dp,$f[i][j]$表示【最后一次选择j,倒数第二次选择i】这种情况发生的概率,那么下一个选择的k必须满足p[k]>p[i] && k > j,并且转移时要把$f[i][j]$乘上转移过去的概率【即在j右边,且p[l]>p[i]的l的个数,记作$num[i][j]$,可以$O(n^2)$预处理】,朴素把这个dp做完是$O(n^3)$的。
进一步优化,考虑从$f[i][j]$到$f[j][k]$的转移,发现在 $ i < j < k $ 的前提下,能够转移到$f[j][k]$的$f[i][j]$只需要满足$p[k] > p[i]$,于是想到在可以每个点都开一个树状数组去协助转移,每次得到$f[i][j]$后,修改j对应的树状数组中的p[i]位置,计算$f[j][k]$则查询j对应的树状数组中$[0,p[k]-1]$的区间和,得到一个$O(n\log n)$的做法,需要常数非常优秀才能通过(例如预处理所有逆元)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long long LL;
const int N = 5e3 + 5;
const int mod = 998244353;
const int MOD = 998244353;
const double pi = acos(-1);
int add(int x, int y){return x + y >= mod? x + y - mod: x + y;}
int sub(int x, int y){return x - y < 0? x - y + mod: x - y;}
int mul(int x, int y){return 1ll * x * y % mod;}
int qpow(int a, int b)
{
int ans = 1;
while(b)
{
if(b & 1)
ans = mul(ans,a);
b >>= 1;
a = mul(a,a);
}
return ans;
}
/////////////////////////////////////////
struct Node {
int v, pos;
bool operator < (Node &N) const {
return v < N.v;
}
}a[N];
int num[N][N], n, p[N];
struct BIT {
#define lowbit(x) (x & (-x))
int sum[N];
void change(int p,int v)
{
while (p<N)
{
sum[p]=add(sum[p],v);
p+=lowbit(p);
}
}
int query(int p)
{
int res=0;
while (p)
{
res=add(res,sum[p]);
p-=lowbit(p);
}
return res;
}
}bit[N];
int ans, invn, inv[N];
int main() {
scanf("%d", &n);
invn = qpow(n, MOD - 2);
for (int i = 1; i <= n; i++) inv[i] = qpow(i, MOD - 2);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i].v);
a[i].pos = i;
}
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) {
p[i] = a[i].pos;
}
for (int i = n; i >= 1; i--) {
for (int j = 1; j <= i; j++) {
num[j][i] = num[j][i + 1] + (p[i + 1] > p[j]);
}
}
for (int i = 1; i <= n; i++) {
num[0][i] = n - i;
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= i - 1; j++) {
int pos = 0;
if (j == 0) {
pos = invn;
}
else {
pos = bit[j].query(p[i] + 1);
}
ans = add(ans, pos);
bit[i].change(p[j] + 2, mul(pos, inv[num[j][i]]));
}
}
printf("%d\n", ans);
return 0;
}
实际上还可以用桶替代树状数组,每次外层循环i修改后,重新扫一遍$f[j][i]$,把原先用树状数组记录的信息记录在桶里,同样可以完成转移,复杂度$O(n^2)$
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long long LL;
const int N = 5e3 + 5;
const int mod = 998244353;
const int MOD = 998244353;
const double pi = acos(-1);
int add(int x, int y){return x + y >= mod? x + y - mod: x + y;}
int sub(int x, int y){return x - y < 0? x - y + mod: x - y;}
int mul(int x, int y){return 1ll * x * y % mod;}
int qpow(int a, int b)
{
int ans = 1;
while(b)
{
if(b & 1)
ans = mul(ans,a);
b >>= 1;
a = mul(a,a);
}
return ans;
}
/////////////////////////////////////////
struct Node {
int v, pos;
bool operator < (Node &N) const {
return v < N.v;
}
}a[N];
int num[N][N], n, p[N];
int ans, invn, inv[N], sum[N], f[N][N];
int main() {
scanf("%d", &n);
invn = qpow(n, MOD - 2);
for (int i = 1; i <= n; i++) inv[i] = qpow(i, MOD - 2);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i].v);
a[i].pos = i;
}
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; i++) {
p[i] = a[i].pos;
}
for (int i = n; i >= 1; i--) {
for (int j = 1; j <= i; j++) {
num[j][i] = num[j][i + 1] + (p[i + 1] > p[j]);
}
}
for (int i = 1; i <= n; i++) {
num[0][i] = n - i;
f[0][i] = inv[n];
}
for (int i = 1; i <= n - 1; i++) {
memset(sum, 0, sizeof(sum));
for (int j = 0; j < i; j++) {
sum[p[j]] = add(sum[p[j]], mul(f[j][i], inv[num[j][i]]));
}
for (int j = 1; j <= n; j++) sum[j] = add(sum[j - 1], sum[j]);
for (int j = i + 1; j <= n; j++) {
f[i][j] = sum[p[j] - 1];
ans = add(ans, f[i][j]);
}
}
printf("%d\n", add(ans, 1));
return 0;
}
其实一开始重新排列也不是必要的,而且也可以从后往前推,并得到一些常数更加优秀的做法
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5010;
const ll mod = 998244353;
int n, p[N];
ll inv[N], f[N][N], sum[N], cnt[N]; //f[i][j]表示上个人选了p[i],当前应选位置>=j的局面的期望轮数
ll qpow(ll a, ll b = mod - 2) {
ll ret = 1;
while(b) {
if(b & 1) ret = ret * a % mod;
b >>= 1; a = a * a % mod;
}
return ret;
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &p[i]), inv[i] = qpow(i);
for(int i = n; i >= 1; i--) {
ll sj = 0, cj = 0;
for(int j = n; j >= 0; j--) {
if(i == j) continue;
if(p[i] > p[j]) {
f[i][j] = (1 + sj * inv[cj]) %mod;
sum[j] = (sum[j] + f[i][j]) % mod;
cnt[j]++;
}
else { //p[i] < p[j]
f[i][j] = (1 + sum[j] * inv[cnt[j]]) % mod;
sj = (sj + f[i][j]) % mod;
cj++;
}
}
}
ll ans = 0;
for(int i = 1; i <= n; i++) ans = (ans + f[i][0]) % mod;
printf("%lld", ans * inv[n] % mod);
return 0;
}
K. Knowledge Test about Match
题意:随机给一个权值范围为0~n-1的序列,用一个0~n-1和它匹配,要求$\sqrt{|a_i-(i-1)|}$之和最小
瞎搞题,枚举差值d从0~n-1,优先匹配差值较小的匹配
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 5;
const int MOD = 998244353;
int n, a[N], b[N];
bool vis[N], vis2[N];
int main() {
int T; scanf("%d", &T);
while(T--) {
scanf("%d", &n);
memset(vis, 0, sizeof(vis));
memset(vis2, 0, sizeof(vis2));
memset(b, 0, sizeof(b));
for (int i = 0; i <= n - 1; i++) scanf("%d", &a[i]);
for (int i = 0; i <= 2 * n; i++) {
for (int j = 0; j <= n - 1; j++) {
if (vis2[j]) continue;
if (a[j] + i <= n - 1 && !b[a[j] + i]) {
b[a[j] + i] = a[j];
vis2[j] = 1;
ans += sqrt(i);
continue;
}
if (a[j] - i >= 0 && !b[a[j] - i]) {
b[a[j] - i] = a[j];
vis2[j] = 1;
ans += sqrt(i);
continue;
}
}
}
for (int i = 0; i <= n - 1; i++) printf("%d ", b[i]);
}
return 0;
}