比赛传送门
作者: fn
题目大意
炉石背景
给定双方英雄血量,随机释放炎爆(打10)直到一名玩家死亡,求获胜概率。
考察内容
数学
分析
分类讨论,设对手的血量最多抗
n
u
m
x
numx
numx 次,自己最多抗
n
u
m
y
numy
numy 次,
获胜的概率即对手抗了
n
u
m
x
numx
numx 次炎爆且自己恰好抗了
0
0
0 次炎爆,
1
1
1 次炎爆,…,
n
u
m
y
−
1
numy-1
numy−1 次炎爆的概率之和。最后一发炎爆留给对手。
计算每种情况的概率并求和即可。
#pragma GCC optimize(3)
#include
#define ll long long
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
const int N=1e6+10; // 抗炎爆的次数<=1e6
const ll mod=998244353;
ll power(ll a,ll b){ // a^b%mod
ll c=1;
for(;b;b>>=1){
if(b&1)c=c*a%mod;
a=a*a%mod;
}
return c;
}
ll n,m,x,y,a[10],b[10];
ll f[N];
ll po2[N*2];
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>x;
for(int i=1;i<=7;i++){
cin>>a[i];
}
cin>>y;
for(int i=1;i<=7;i++){
cin>>b[i];
}
swap(x,y); //
ll numx=(x+10-1)/10; // 可以抗炎爆的次数
ll numy=(y+10-1)/10;
ll ans=0;
po2[0]=1;
for(int i=1;i<=numx+numy;i++){ // 预处理2的幂次
po2[i]=po2[i-1]*2%mod;
}
ll sumx=1; // 1*2*...*(numx-1)
for(int i=2;i<=numx-1;i++){
sumx=sumx*i%mod;
}
ll invsumx=power(sumx,mod-2); // sumx的逆元
ll sum=sumx;
for(int i=0;i<=numy-1;i++){ // 枚举y被打中的次数
ll cnt=sum*invsumx%mod; // C(i+numx-1,numx-1)%mod;
sum=sum*(i+1+numx-1)%mod;
sum=sum*power(i+1,mod-2)%mod; // 除以(i+1)
cnt=cnt*power(po2[numx-1+i],mod-2)%mod;
ans+=cnt;
ans%=mod;
}
cout<<ans%mod*499122177%mod<<endl; // *(1/2)
return 0;
}
/*
30 0 0 0 0 0 0 0
30 0 0 0 0 0 0 0
20 0 0 0 0 0 0 0
30 0 0 0 0 0 0 0
*/
题目大意
给定一个可以包含重边的无向图,初始在点
s
s
s ,两个玩家轮流行动,先手每次可以移除一条和所在位置相邻的边, 后手每次可以沿着一条未删除边移动,如果可以移动到
t
t
t 则后手获胜,否则先手获胜。
求双方最优策略下的胜者。
考察内容
博弈论,bfs
分析
终点是一个必胜态。
如果当前点
x
x
x 到终点
t
t
t 有两条边,则无论切掉哪一条都能走到终点
t
t
t ,所以
x
x
x 也是必胜态,加入必胜的点的集合。
如果当前点 y y y 到必胜的点有两条边,则 y y y 也是必胜的点,加入必胜的点的集合。
从终点 t t t 开始跑bfs,找到所有必胜的点的集合,剩下的就是必败的点。判断 s s s 是否在集合中即可。
#pragma GCC optimize(3)
#include
#define ll long long
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
const int N=105;
ll n,m,s,t;
vector<ll> g[N];
int vis[N]; // 是否在取胜集合中
ll num[N]; // 取胜的路径数量
queue<ll> q;
void bfs(){
vis[t]=1; // 进入取胜集合
q.push(t);
while(!q.empty()){
ll t1=q.front();
q.pop();
for(auto a1:g[t1]){
if(vis[a1])continue;
num[a1]++;
if(num[a1]>=2){ // 路径数量>=2
q.push(a1);
vis[a1]=1;
}
}
}
}
void init(ll n){ // 初始化
memset(vis,0,sizeof(vis[0])*(n+1));
memset(num,0,sizeof(num[0])*(n+1));
for(int i=1;i<=n;i++){
g[i].clear();
}
}
int main(){ // bfs
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t0; cin>>t0;
while(t0--){
cin>>n>>m>>s>>t;
init(n);
for(int i=1;i<=m;i++){
ll u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
bfs();
if(!vis[s])cout<<"Cut Player"<<endl; // 不在取胜集合中,先手获胜
else cout<<"Join Player"<<endl; // 后手获胜
}
return 0;
}
/*
1
4 6 1 4
1 2
1 3
2 4
2 4
3 4
3 4
*/
题目大意
给定两个序列
a
,
b
a,b
a,b ,找出下标
i
,
j
,
k
,
l
i,j,k,l
i,j,k,l ,使得
a
[
i
]
+
b
[
l
]
=
a
[
j
]
+
b
[
k
]
a[i]+b[l]=a[j]+b[k]
a[i]+b[l]=a[j]+b[k] 且
i
≠
j
,
k
≠
l
i≠j,k≠l
i=j,k=l 。若不存在则输出-1。
0
≤
a
[
i
]
,
b
[
i
]
≤
1
0
7
0 \leq a[i],b[i] \leq 10^7
0≤a[i],b[i]≤107
考察内容
鸽笼原理,枚举
分析
亲测直接FFT或NTT等复杂度带log的做法过不了。。。(又被标题骗了。。。)
假设
a
,
b
a,b
a,b 中均无重复元素。
由鸽笼原理, 因为
a
[
i
]
+
b
[
l
]
≤
V
≤
2
∗
1
0
7
a[i]+b[l] \leq V \leq 2*10^7
a[i]+b[l]≤V≤2∗107 ,所以遍历不同的元素
O
(
V
)
O(V)
O(V) 次就一定可以找到一组答案。
把
a
、
b
a、b
a、b 去重之后进行枚举即可。注意考虑两对相同元素的情况。
时间复杂度
O
(
V
)
O(V)
O(V) 。
#pragma GCC optimize(3)
#include
#define ll long long
#define cer(x) cerr<<(#x)<<" = "<<(x)<<'\n'
#define endl '\n'
using namespace std;
const int N=1e6+5;
const int MM=2e7+5;
int n,a[N],b[N],m;
int n1,A[N]; // A为去重后的a数组的下标
int nx[MM],ny[MM];
int vis[MM>>1]; // vis[i]记录i在a/b中的下标
int ggx=0,ggy; // 记录a中一对相同元素的下标
bool ok=0;
int main(){ // 鸽笼原理。复杂度O(V)。AC
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%d",&a[i]);
if(vis[a[i]]){ // a[i]之前已经出现过
if(ggx==0){ // 以前没发现对子
ggx=vis[a[i]]; // 上一个a[i]的下标
ggy=i; // 这个a[i]的下标
}
}
else{ // a[i]第一次出现
A[++n1]=i; // 记录去重后的a数组的下标
}
vis[a[i]]=i; // 记录a[i]出现的下标
}
for(int i=1;i<=m;i++) {
scanf("%d",&b[i]); // 读入b[i]
}
memset(vis,0,sizeof vis); // 清空vis,接下来vis[i]用来记录i在b中的下标
for(int i=1;i<=m;i++) { // 遍历b[i]
if(vis[b[i]]) { // b[i]出现过
if(ggx) { // a中存在"对子"
printf("%d %d %d %d\n",ggx,ggy,vis[b[i]],i); // 输出两对相同元素
ok=1; // 找到了
break;
}
continue;
}
// vis[b[i]]==0,b[i]第一次出现
vis[b[i]]=i;
for(int j=1;j<=n1;j++) { // 枚举a中的元素(已通过A[i]去重)
int u=1e7; // 偏移量
int o=b[i]-a[A[j]]+u; // 计算nx的下标
if(nx[o]){ // 找到一组"碰撞"
printf("%d %d %d %d\n",nx[o],A[j],ny[o],i);
ok=1;
break;
}
nx[o]=A[j];
ny[o]=i;
}
if(ok) break; // 已经找到,提前退出
}
if(!ok) puts("-1"); // 没找到,输出-1
}
题目大意
有
n
n
n 个审稿人和
m
m
m 篇论文,给出每个审稿人能审论文的集合,给每个审稿人安排一篇论文。
令
f
(
i
)
f(i)
f(i) 表示被至少
i
i
i 个审稿人审过的论文数量,要求求出一种分配方案,使得
(
f
(
1
)
,
f
(
2
)
,
.
.
.
,
f
(
n
)
)
(f(1),f(2),...,f(n))
(f(1),f(2),...,f(n)) 字典序最大。
1 ≤ n , m ≤ 400 1≤n,m≤400 1≤n,m≤400
考察内容
二分图匹配,匈牙利算法,最大流,费用流
分析
解法一(最小费用最大流):
建最小费用流图:
源点向每个审稿人连接容量为
1
1
1 ,花费为
0
0
0 的边;
每个审稿人向能审的论文连接容量为
1
1
1 ,花费为
0
0
0 的边;
每篇论文向汇点连接
n
n
n 条容量为
1
1
1 ,花费为
1
,
2
,
.
.
.
,
n
1,2,...,n
1,2,...,n 的边。
然后计算最小费用最大流。
解法二(二分图匹配):
注意到要求
f
(
1
)
f(1)
f(1) 最大就是求二分图最大匹配,可以用匈牙利算法或最大流解决。
对于
i
=
1
,
2
,
.
.
.
,
n
i=1,2,...,n
i=1,2,...,n ,在不改变
f
(
1
)
,
f
(
2
)
,
.
.
.
f
(
i
−
1
)
f(1),f(2),...f(i-1)
f(1),f(2),...f(i−1) 的情况下最大化
f
(
i
)
f(i)
f(i) 。
解法二代码:
#include
using namespace std;
bool Mark[405];
bool Vis[405];
int Fa[405],Cnt[405];
vector<int> Edge[405];
bool Hungary(int u) { // 匈牙利算法(增广路算法)
for(int i=0; i<Edge[u].size(); i++) {
int v=Edge[u][i];
if(!Mark[v]) {
Mark[v]=1;
if(Fa[v]==-1 || Hungary(Fa[v])) {
Fa[v]=u;
return true;
}
}
}
return false;
}
int n,m;
string S;
int main() { // 匈牙利算法求二分图匹配
memset(Fa,-1,sizeof(Fa));
scanf("%d%d",&n,&m);
bool flag=1;
for(int i=1; i<=n; i++) {
cin>>S;
bool f=0;
for(int j=0; j<m; j++){
if(S[j]=='1'){ // 可以看这篇论文
Edge[j+1].push_back(i); // 连边
f=1;
}
}
flag&=f;
}
if(!flag) {
puts("-1");
return 0;
}
int mx=0; // 记录cnt的最小值
bool f=1;
while(f) {
f=0;
for(int i=1; i<=m; i++) { // 枚举每一篇论文
if(Cnt[i]!=mx)continue;
memset(Mark,0,sizeof(Mark));
if(Hungary(i)){
Cnt[i]++;
f=1;
}
}
mx++;
}
for(int i=1; i<=n; i++)
printf("%d ",Fa[i]);
puts("");
}
2022暑期多校联赛完结撒花!