0%

2022牛客多校训练营10

F-Shannon Switching Game?

来源: 牛客多校训练营10 算法: DFS, 博弈论 题目链接: https://ac.nowcoder.com/acm/contest/33195/F 补完: Yes 完成时间: August 20, 2022

题解

给无向图和起始点, 轮流删边和移动, 问可否抵达.

发现想要到达终点t, 从上一个点t1出发, 则在t1t之间必须要有多重边, 若仅为单边则在删去该条边后无法抵达.

递归可得想要从点ti1出发直接到达到达点ti, 则必须有点ti1ti之间有多重边.

对于如此形成的多重边链tik, 发现对于不在链上的点, 若可达一点即为可达其余带点, 想要任意点可达多重边链, 则必须有该点有多条边连向多重边链.

使用DFS从终点构造可达图, 判断起点是否在图上即可.

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<bits/stdc++.h>
#define rep(i, a, b) for (int i = (a); i <= (b); ++i)
using namespace std;

const int MAXN = 10005;

int n, m, s, t;
int u, v;
vector<int> e[MAXN];
int vis[MAXN];

void dfs(int u) {
vis[u]++;
if (vis[u]!=2)return;
for (auto v : e[u])dfs(v);
}

void solve() {
cin>>n>>m>>s>>t;
rep (u, 1, n) {
e[u].clear();
vis[u] = 0;
}
rep (i, 1, m) {
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
vis[t]++;
dfs(t);
if(vis[s]>=2) printf("Join Player\n");
else printf("Cut Player\n");
}

int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T = 1;
cin>>T;// 无多组数据注释这一行
while (T--) {
solve();
}
return 0;
}

H-Wheel of Fortune

来源: 牛客多校训练营10 算法: 排列组合, 逆元 题目链接: https://ac.nowcoder.com/acm/contest/33195/H 补完: Yes 完成时间: August 20, 2022

题解

设我受A次攻击死亡, 对方受B次攻击死亡, 则想赢得游戏需要在我受A次攻击前, 对方先受B次攻击, 此过程与随从无关, 若不攻击随从, 击中我们和对方的概率都为12.

则赢得游戏时, 对方已经受到B次攻击, 设我已经受到k次攻击, 则我们总共被攻击了B+k次, 发生概率为:

(12)B+k

B+k次攻击中最后一次必是是对方受击, 则其余的B+k1次攻击中有B1次攻击对方, 在B+k1次攻击中任取B1次攻击, 为组合数, 故有概率:

CB+k1B1(12)B+k

只要我被攻击的次数不超过A就可以赢得游戏, 故我有A种赢法, 分别为我受0A1次攻击, 累加得答案:

ans=k=0A1CB+k1B1(12)B+k

公式整理出:

ans=k=0A1(B+k1)!(B+k)!k!2B+k

阶乘和幂提前计算, 同余除法用逆元处理.

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include<bits/stdc++.h>
#define rep(i, a, b) for (int i = (a); i <= (b); ++i)
using namespace std;

#define ll long long

const ll P = 998244353;
const int MAXN = 2000005;

ll Add(ll a, ll b){
return (a + b + P) % P;
}
ll Sub(ll a, ll b){
return (a - b + P) % P;
}
ll Mul(ll a, ll b){
return (a * b) % P;
}
ll Pow(ll a, ll n){
ll ans = 1;
while (n) {
if (n & 1) ans = Mul(a, ans);
a = Mul(a,a);
n >>= 1;
}
return ans;
}
ll Div(ll a, ll b){
b = Pow(b, P - 2);
return Mul(a, b);
}

ll fact[MAXN];
void initFact(){
fact[0] = 1;
for (int i = 1; i<= MAXN; ++i) fact[i] = Mul(fact[i-1], i);
}
ll pow2[MAXN];
void initPow2(){
pow2[0] = 1;
for (int i = 1; i<= MAXN; ++i) pow2[i] = Mul(pow2[i-1], 2);
}

ll temp;
ll A, B;
ll ans;

void solve() {
cin>>A;
rep (i, 1, 7) cin>>temp;
cin>>B;
rep (i, 1, 7) cin>>temp;
A = (A + 9) / 10;
B = (B + 9) / 10;
initFact();
initPow2();
rep (k, 0, A-1) {
ans = Add(ans, Div(fact[B-1+k], Mul(Mul(fact[B-1], fact[k]), pow2[B+k])));
}
cout<<ans;
}

int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T = 1;
// cin>>T;
while (T--) {
solve();
}
return 0;
}

I-Yet Another FFT Problem?

来源: 牛客多校训练营10 算法: 抽屉原理, 模拟 题目链接: https://ac.nowcoder.com/acm/contest/33195/I 补完: Yes 完成时间: August 26, 2022

题目链接

题意简述

给定两个数组,a和b,问是否存在1i,jn,1k,lm, 其中i和j不相等,k和l不相等,使得|aiaj|=|bkbl|

输出ijkl

nm1e6范围内,数组内数在1e7范围内。

题目分析

本题题意非常清晰,找到两个数组内的两对数差值相等。

但很显然,朴素地去枚举差值再去判断相等,时间上肯定来不及。

尽管记录一个数组中的结果,让第二个数组查询是否有解的时候可以做到O(1)的复杂度,但在一个数组中枚举数对做差就是n2级别的复杂度了,且这个过程需要进行两次。

那么如何提升效率呢?

如果把要求中的|aiaj|=|bkbl|,变成ai+bl=bk+aj呢?

(由于下标没有谁大于谁的限制,所以不管去掉绝对值是否需要倒置,结果都没什么差别。)

这样一个移项的变换,把枚举同个数组中的数对变为枚举两个数组中的数对,看起来好像完全没有优化到。

然而,需要注意的是,在数据量比较大的时候,做差存储仍然是n2级别,可以预见的,数据中会有不少的重复。(意思是一个差值在多个数对中出现)

但是,做和的复杂度将会大大下降。

做和不仅不需要像做差一样,一个数组的所有数对枚举标记之后才能进入判断,而是在枚举过程中就可以和先前的结果进行比对;更重要的是,由于数据范围在1e7,那么和的范围是2e7,假设,跑的时候实在脸黑,每次都没找到和前面一样的结果,那最多1跑到2e7,每个数字都跑出来一次,那再跑一次,由于数据在这个范围内,一定会出现重复,有了一组结果,遍历就可以结束了。

挺耳熟吧?是的,这是抽屉原理

方案有了,思考一下操作起来的细节。

首先这个数据还是不小的,我们可以用桶数组去重。(代码中的cntacntb),由于我们只需要一组合法的数对,那么原始数据中重复的数字,只要保留一个就行。由于题目要输出下标,使用结构体记录下标以防丢失。

接着,考虑到重复数字可能构成差为0的情况,加入标记变量(代码中的flagaflagb)记录这种情况,特殊处理一下,可以更快跑出这种特殊情况的解。并且处理掉相同数字带来的答案也是每个数字只保留一个的前提。(或者不跑这个的话去重的时候每个数字留最多两个也行)

最后,在对比的时候,怎么确定这个数对有没有出现过呢?

和的范围在2e7内,勉强还能开出这个规模的数组。一个数组记录访问情况,另一个结构体用法类似桶,记录各个和对应的坐标即可。

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include<iostream>
using namespace std;
struct number{
int num;
int id;
}a[1000005],b[1000005];
int cnta[10000005]={0},cntb[10000005]={0};
int visit[20000005]={0};
struct mark{
int i;
int j;
}mk[20000005];
int main(){
int n,m;
cin>>n>>m;
int stpa=0,stpb=0;
int flaga=0,flagb=0;
int numa,numb;
for(int i=1;i<=n;i++){
int num;
cin>>num;
if(cnta[num]==1){
//已经访问过即是第二次出现,标记后跳过。
if(flaga!=0)continue;
flaga=i;
numa=num;
continue;
}
cnta[num]++;
a[stpa].num=num;
a[stpa++].id=i;
}

for(int i=1;i<=m;i++){
int num;
cin>>num;
if(cntb[num]==1){
if(flagb!=0)continue;
flagb=i;
numb=num;
continue;
}
cntb[num]++;
b[stpb].num=num;
b[stpb++].id=i;
}

if(flaga!=0&&flagb!=0){//输出差值为0的答案
for(int i=0;i<stpa;i++)
if(a[i].num==numa){
cout<<a[i].id<<' '<<flaga<<' ';
}
for(int i=0;i<stpb;i++)
if(b[i].num==numb){
cout<<b[i].id<<' '<<flagb<<'\n';
}
return 0;
}

for(int i=0;i<stpa;i++){
for(int j=0;j<stpb;j++){
int ad=a[i].num+b[j].num;
if(visit[ad]){
cout<<mk[ad].i<<' '<<a[i].id<<' '<<mk[ad].j<<' '<<b[j].id<<'\n';
return 0;
}
visit[ad]=1;
mk[ad].i=a[i].id;//记录和为ad的数对的坐标
mk[ad].j=b[j].id;
}
}
cout<<"-1";
return 0;
}