1608C - Game Master
题目描述:
现在有 n n n 个选手正在进行比赛,其中第 i i i 个选手在两张地图中的能力值分别为 a i , b i a_i,b_i ai,bi。现在要进行 n − 1 n-1 n−1 场比赛来角逐出一个胜者,每场比赛可以任意安排当前未被淘汰的选手中任选两个在任意一张地图中比赛,其中在该地图能力值高的获胜(题目保证每张地图中所有人的能力都不同),问所有 n n n 个人是否都有机会成为最后胜者
1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^5 1≤n≤105
solution:
这题实际上是一个隐式图问题,把 i i i 个选手抽象成 i i i 个点,当且仅当 i i i 的能力值在任意一张地图大于 j j j 时,从 i i i 连向 j j j。
做法1:
可以发现期望边数可达到 O ( n 2 ) O(n^2) O(n2),如果要硬建,考虑线段树优化建图,以能力值为下标开两棵线段树,每次从这个人的点向能力比他大的这段区间连边即可,并且维护某一能力值最大的点所在连通块,该连通块内的人都可能成为胜者。
做法2:
由于边的本质是一种偏序关系,具有传递性,如假设我们这样建边 A − > B , B − > C A->B,B->C A−>B,B−>C,那么显然 A − > C A->C A−>C 这条边就是多余的了。因此我们依次按照 a i , b i a_i,b_i ai,bi 排序,接着 O ( n ) O(n) O(n) 对所有相邻两点建边即可。
显然只有当一个点可以达到能力值最高的点时,说明该点能成为胜者。因此我们整张图是强连通分量,考虑 Tarjan 缩点。得到 DAG 后,显然最后入度为 0 0 0 的强连通分量有且仅有一个,在这个强连通分量中的意义就是能走到能力值最高的点,那么有且只有这里面的所有点可能成为胜者。
做法3:
上述两种做法本质上是为了找能到达最高能力值的点,如果不用上述技巧,我们只能暴力的dfs 每个点去尝试。如果 u u u 能走到能力值最高的点,那么一定存在一条单向路径。于是我们考虑建反图,那么只需要从能力值最高的点出发,看看能否到达每个点即可。
做法4:
考虑序列上的做法,我们以 a a a 为关键字降序排序,显然排序后的对于第 i i i 个点,所有 [ i + 1 , n ] [i+1,n] [i+1,n] 个点都可以直接通过 a i a_i ai 碾压获胜,这也代表对于第 i i i个点,我们当前手上可操控的最大的 b i b_i bi 存在于 [ i , n ] [i,n] [i,n],这也是我们打败 [ 1 , n − 1 ] [1,n-1] [1,n−1] 唯一的砝码。
我们需要在 [ 1 , n − 1 ] [1,n-1] [1,n−1] 找一个可以成为最后胜者,并且 b i b_i bi 值小于上述最大值的点。我们分别维护 p r e i pre_i prei 与 s u f i suf_i sufi 代表前 i i i 个点中能成为胜者的点的最小 b i b_i bi 与后 [ i , n ] [i,n] [i,n] 个点中的最大 b i b_i bi 值,显然只要 p r e i − 1 < s u f i pre_{i-1}<suf_i prei−1<sufi,当前点 i i i 就能成为胜者。并且对于 p r e 1 pre_1 pre1 来说显然由于其 a 1 a_1 a1 值碾压,所以必胜。
做法5:
考虑那些必败者,要么打不过任何人,要么能打过的人打不过任何人,要么能打过的人能打过的人打不过任何人,以此类推,考虑优化
我们分别对 a i a_i ai 与 b i b_i bi 进行降序排序,如果存在一个位置(且是第一次出现这种情况的位置) i i i 使得在两边前 i i i 强的都是这 i i i 个人,那么其内部每个人都能成为胜者,且集合外的人都无法战胜他们
code(tarjan 缩点):
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=3e5+10;
int n;
struct node{
int a,b,id;
}a[maxn];
bool cmp1(node x,node y){
return x.a<y.a;
}
bool cmp2(node x,node y){
return x.b<y.b;
}
vector<int> g[maxn];
int dfn[maxn],low[maxn],cnt,cl,col[maxn];
bool vis[maxn],qs[maxn];
int in[maxn];
stack <int> st;
void tarjan(int x) {
dfn[x]=low[x]=++cnt;
vis[x]=1;
st.push(x);
for(int i=0;i<g[x].size();i++) {
int v=g[x][i];
if(!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
if(vis[v]) low[x]=min(low[x],dfn[v]);
}
if(dfn[x]==low[x]) {
++cl;
int tt;
do{
tt=st.top();
vis[tt]=0;
col[tt]=cl;
st.pop();
}while(tt^x);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int t;
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++) g[i].clear(),vis[i]=qs[i]=dfn[i]=low[i]=col[i]=0;
for(int i=1;i<=n;i++) cin>>a[i].a;
for(int i=1;i<=n;i++) cin>>a[i].b,a[i].id=i;
sort(a+1,a+n+1,cmp1);
for(int i=1;i<n;i++) g[a[i+1].id].push_back(a[i].id);
sort(a+1,a+n+1,cmp2);
for(int i=1;i<n;i++) g[a[i+1].id].push_back(a[i].id);
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++){
for(int j=0;j<g[i].size();j++){
int v=g[i][j];
if(col[v]!=col[i]) ++in[col[v]];
}
}
for(int i=1;i<=cl;i++){
if(!in[i]) qs[i]=1;
}
for(int i=1;i<=n;i++) cout<<qs[col[i]];
cout<<endl;
}
return 0;
}
1608D - Dominoes
题目描述:
有 n n n 个分左右格的多米诺骨牌,要求黑白染色,有些骨牌的左格或右格子已被染色,其余的待染色。问存在几种涂色方案使得涂色后的骨牌重排后满足任意第 i i i 个骨牌的右半边和第 ( i m o d n ) + 1 (i \mod n)+1 (i mod n)+1 个骨牌的左半边颜色不同。
solution:
由于上述约束条件,黑色和白色是成对出现的,也就是说合法方案中必定存在 n n n 张黑色与 n n n 张白色,并且倘若某张骨牌是纯黑/纯白,那么一定对应的会存在一张纯白/纯黑的骨牌,也就是说合法方案中 “WW” 与 “BB” 数量相等。
同一种类型的 “WB” “BW” 骨牌可以共存,下一张永远可以是相同的牌
对于有且仅有这两种 “WB” “BW" 类型的骨牌的情况下,不存在一种方案使得这两种不同类型的骨牌可以相互衔接,也就是说这两种骨牌需要纯色骨牌的过渡才得以排列,并且由于环形结构,在过渡一次后后续需要过渡回来。由于上述发现 “WW” 与 ”BB“ 数量相等,那么意思就是如果 ”WB“ 与 ”BW” 同时存在,那么此时必须要有纯色牌(充要条件)使得方案合法。
也就是说如果原始涂色情况中有纯色牌的存在,我们只需要保证黑色与白色数量相等,那么方案一定是合法的,设黑色数量为 b b b,白色数量为 w w w,方案显然就是 C 2 n − w − b n − w C_{2n-w-b}^{n-w} C2n−w−bn−w
那么如果没有,我们需要考虑减去不合法的方案,也就是那些有且仅有这两种 “WB” “BW" 类型的骨牌的情况,发现不太好算。那么正难则反,该情况本质就是没有纯色骨牌的情况减去全是 “WB” 和全是 “BW” 的情况。
没有涂色骨牌的数量很好算,如果一个骨牌只有一个格子没涂色或都涂色,显然只有一种方案,如果两个格子都没涂色,那么有两种,满足乘法原理。而如果原来的骨牌中不存在 “WB” “BW" 类型对立的情况中的任意一种颜色子序列,就可能出现全是这两种 “WB” “BW" 类型的骨牌的情况。
code:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=3e5+10;
const int p=998244353;
#define endl '\n'
int n;
string s[maxn];
ll fac[maxn],ifac[maxn];
ll qpow(ll a,ll b){
ll res=1;
while(b){
if(b&1) res=res*a%p;
a=a*a%p;
b>>=1;
}
return res;
}
ll cal(int n,int m){
return fac[n]*ifac[m]%p*ifac[n-m]%p;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n;
fac[0]=1;
for(int i=1;i<=2*n;i++) fac[i]=fac[i-1]*i%p;
ifac[2*n]=qpow(fac[2*n],p-2);
for(int i=2*n-1;i>=0;i--){
ifac[i]=ifac[i+1]*(i+1)%p;
}
bool f=0;
int w=0,b=0,cnt=0;
int c1=0,c2=0;
for(int i=1;i<=n;i++){
cin>>s[i];
if(s[i]=="BB"||s[i]=="WW") f=1;
if(s[i][0]=='W') w++;
if(s[i][1]=='W') w++;
if(s[i][0]=='B') b++;
if(s[i][1]=='B') b++;
if(s[i]=="??") cnt++;
if(s[i][0]=='W'||s[i][1]=='B') c1++;
if(s[i][0]=='B'||s[i][1]=='W') c2++;
}
if(w>n||b>n){
cout<<0<<endl;
return 0;
}
if(f){
cout<<cal(2*n-w-b,n-w)<<endl;
return 0;
}
ll ans=cal(2*n-w-b,n-w);
ans=(ans-qpow(2,cnt)+p)%p;
if(!c1) ans++;
if(!c2) ans++;
cout<<ans<<endl;
return 0;
}