10.18 正睿模拟赛题解
T1
根据排序不等式,正序最大,所以这个值就是排序后相加即可。而交换次数可以用置换环个数来实现。这是一个经典套路。
注意,只有当数两两不同的时候,我们才能够数置换环。一开始出题人没有说两两不同,我多耽误了 \(30\) 分钟。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 300010
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=998244353;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
inline int TwoPow(int x){return 1ll*x*x%mod;}
int n,T,a[N],b[N],c[N],d[N];
int li[N];
bool vis[N];
int main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);read(T);
for(int i=1;i<=n;i++){read(a[i]);c[i]=a[i];}
for(int i=1;i<=n;i++){read(b[i]);d[i]=b[i];}
sort(c+1,c+n+1);sort(d+1,d+n+1);
ll ans=0;
for(int i=1;i<=n;i++){
ans=(ans+TwoPow(c[i]-d[i]))%mod;
}
printf("%lld ",ans);
if(T==0) exit(0);
int len=unique(c+1,c+n+1)-c-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(c+1,c+len+1,a[i])-c;
len=unique(d+1,d+n+1)-d-1;
for(int i=1;i<=n;i++) b[i]=lower_bound(d+1,d+len+1,b[i])-d;
for(int i=1;i<=n;i++){
li[a[i]]=b[i];
}
int cnt=0;
for(int i=1;i<=n;i++){
if(vis[i]) continue;
cnt++;int now=i;
while(!vis[now]){
vis[now]=1;now=li[now];
}
}
printf("%d\n",n-cnt);
return 0;
}
T2
对于所有主教,我们先计算其主对角线的覆盖区间,对于副对角线,不难发现其与主对角线的焦点形成了一个区间,直接维护前缀和即可。
这个题考试的时候没有想出来,思考过如何维护交点,没有发现后面的性质,考试的时候错误的用线段树维护了交点,不仅提高了代码复杂度,而且还写假了。一定要充分考虑对错之后在动笔。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 1000100
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int base=1000000;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int sum[N<<1],n,m;
ll ans;
typedef pair<int,int> P;
P a[N];
bool vis[N<<1];
//id=0 是上面,id=1 是下面。
inline P GetPoint(int x,int y,int id){
int R=x-y;
if(R<0){
if(id==0) return make_pair(1,1-R);
else if(id==1) return make_pair(n+R,n);
}
else{
if(id==0) return make_pair(R+1,1);
else if(id==1) return make_pair(n,n-R);
}
assert(0);
}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);read(m);
for(int i=1;i<=m;i++){
// printf("i=%lld\n",i);
int x,y;read(x);read(y);
if(!sum[x+y]){
sum[x+y]=1;
if(x+y>n+1) ans+=(2*(n+1)-x-y-1);
else ans+=x+y-1;
}
a[i]=make_pair(x,y);
// printf("ans=%lld\n",ans);
}
// printf("ans=%lld\n",ans);
for(int i=2;i<=(n<<1);i++){
sum[i]+=sum[i-2];
}
for(int i=1;i<=m;i++){
if(vis[a[i].first-a[i].second+base]) continue;
vis[a[i].first-a[i].second+base]=1;
P up=GetPoint(a[i].first,a[i].second,0);
P down=GetPoint(a[i].first,a[i].second,1);
int l=up.first+up.second,r=down.first+down.second;
ans-=sum[r]-sum[l-2];
ans+=down.first-up.first+1;
}
printf("%lld",1ll*n*n-ans);
return 0;
}
T3
这个概率题比较巧妙,是属于那种看起来转移带环,但实际上不带环的那种。
首先考虑这张图是基环树森林,首先考虑树上的情况:
这个式子一开始没有理解,认为后面不需要乘上 \(1-p_i\),但事实上,我们需要把情况分的很清楚,可以通过划分集合来理解,第一个集合是陨石能感染 \(i\),第二种情况是陨石感染不了 \(i\),这就是上面的转移。
也就是说,我们需要把情况分的很细,必须要划分清除。
实际上在做的时候我们也可以这样转移:
这样正确性也是显然的,并且更好转移,\(f_i\) 的初始值就是 \(p_i\)。
然后我们考虑环,不难发现,这个转移是不带环的。我们考虑一个环:
我们关注 \(1\) 号节点感染的概率,显然在做完树形 dp 之后我们可以把 \(f_1\) 看做 \(1\) 号节点被自身感染(即不被环上的点感染的概率)的概率直接代入转移。
转移式为:
这个转移是正确性比较显然,只需要注意我们上面所说的集合划分,我们要考虑优先级,不难理解为什么要乘上 \((1-f_1),(1-f_1)(1-f_2)\),如果暴力对每个点做的话,复杂度将会是 \(O(n^2)\),这是因为环的长度可以到达 \(O(n)\)。我们考虑快速对每个点做上面这个事情。
不难发现,上面这个式子其实是一个迭代形式,它可以转化成:
如果我们把 \(f_2,f_3\) 的式子也写出来,不难发现也是一个迭代形式,只不过顺序不同而已,而且我们发现,这个顺序好像是有规律的。
我们考虑这个迭代顺序怎么做。
不难发现,乘法和加法可以用矩阵乘法来维护,只要适当构造即可,那么所谓顺序不同,就是矩阵乘法不同而已。
我们维护矩阵乘法后缀积,前缀积,就可以做完这道题了。
今天学到了矩阵乘法加速迭代,可以方便改变迭代顺序。
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 100010
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=998244353;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
inline int ksm(int a,int b,int mod){
int res=1;while(b){if(b&1) res=1ll*res*a%mod;a=1ll*a*a%mod;b>>=1;}return res;
}
struct Matrix{
int a[2][2];
inline void Clear(){memset(a,0,sizeof(a));}
inline Matrix operator * (const Matrix &b)const{
Matrix c;c.Clear();
for(int i=0;i<=1;i++)
for(int j=0;j<=1;j++)
for(int k=0;k<=1;k++) c.a[i][j]=(c.a[i][j]+1ll*a[i][k]*b.a[k][j]%mod)%mod;
return c;
}
}ma[N],E;
inline int Frac(int a,int b){
return 1ll*a*ksm(b,mod-2,mod)%mod;
}
int p[N],q[N],t[N],n,rd[N];
bool vis[N];
queue<int> qu;
inline void Init(){
read(n);
for(int i=1;i<=n;i++){
int a,b;read(a);read(b);
p[i]=Frac(a,b);
}
for(int i=1;i<=n;i++){read(t[i]);rd[t[i]]++;}
for(int i=1;i<=n;i++){
int a,b;read(a);read(b);
q[i]=Frac(a,b);
}
}
inline void Bfs(){
for(int i=1;i<=n;i++) if(!rd[i]) qu.push(i);
while(qu.size()){
int top=qu.front();qu.pop();
vis[top]=1;
rd[t[top]]--;if(!rd[t[top]]) qu.push(t[top]);
p[t[top]]=(1-1ll*(1-1ll*p[t[top]])*(1-1ll*p[top]*q[top]%mod)%mod)%mod;
}
}
inline Matrix Dfs(int k,Matrix val){
if(vis[k]) return E;vis[k]=1;
Matrix now=ma[k]*Dfs(t[k],val*ma[k]);
p[k]=(now*val).a[1][0];return now;
}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
// cout<<(ll)1ll*11*ksm(16,mod-2,mod)%mod;
Init();Bfs();
for(int i=1;i<=n;i++){
ma[i].a[0][0]=1ll*(1-(ll)p[t[i]])*q[i]%mod;
ma[i].a[0][1]=0;ma[i].a[1][1]=1;
ma[i].a[1][0]=p[t[i]];
}E.a[0][0]=E.a[1][1]=1;
for(int i=1;i<=n;i++) if(!vis[i]) Dfs(i,E);
for(int i=1;i<=n;i++){
printf("%lld ",(p[i]%mod+mod)%mod);
}
return 0;
}
T4
首先我们发现正着不好做,我们考虑做补集,于是问题转化成了给定一张有向图,求点集的导出子图为 \(DAG\) 的方案数。
点数很少,边数很多,考虑状压。因为是 \(DAG\),我们考虑其度数为 \(0\) 节点构成的集合 \(T\)。
设 \(f_S\) 为当点集为 \(S\) 的时候的构成 \(DAG\) 方案数。
如果我们强制钦定 \(T\) 集合中的点度数都为 \(0\),那么从集合 \(T\) 到 \(S-T\) 可以有边,但是从 \(S-T\) 到 \(T\) 不能有边,所以我们有一个式子:
但是不难发现上面这个方案会算重,因为 \(f_{S-T}\) 中的某些方案会让 \(S-T\) 中的点的度数为 \(0\),这并不是我们想要的。我们考虑容斥。
令 \(q_T=2^{way(T,S-T)}f_{S-T}\),令 \(S\) 点集中恰好有集合 \(R\) 中的点为 \(S\) 中度数为 \(0\) 的点,即 \(S\) 中度数为 \(0\) 的点恰好是 \(R\) 中的点的方案数为 \(p_R\),那么我们有:
注意这里需要满足 \(R\subseteq S\)。
稍微解释一下上面的这个式子,也就是说,对于 \(q_T\) 中的每一种方案,我们把 \(T\) 和 \(S-T\) 中的 \(0\) 度点全部放进 \(R\),这样,每一种方案就会在 \(p_R\) 中计数一次。这个式子的正确性是显然的,注意 \(T\) 是不会变的。
运用二项式反演的子集集合,我们可以得到:
而在本题中,我们要求的相当于:
所以我们有:
然后就可以转移 \(S\) 了。注意我们如何预处理 \(way(R,S-R)\) 这个东西,我们考虑对每个 \(S\) 都重新预处理,不难发现,我们只需要每次增加一个点就可以转移。因为 \(way(R,S-R)\) 的预处理,我们需要注意一下 dp 顺序,我们令 \(g_R=way(R,S-R)\),我们考虑 \(R\) 要从小到大枚举。
代码:
#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 20
#define M number
using namespace std;
const int INF=0x3f3f3f3f;
const int mod=1e9+7;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
inline int lowbit(int x){return x&(-x);}
int lg2[1<<N],Cnt[1<<N],Pre[N],Next[N],n,m,g[1<<N],f[1<<N],Pow[N*N];
inline int sgn(int a){if(a&1) return -1;return 1;}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);read(m);
//点的编号是 0 到 n-1
for(int i=1;i<=m;i++){
int from,to;read(from);read(to);
from--;to--;
Pre[to]|=(1<<from);Next[from]|=(1<<to);
}
lg2[0]=-1;Pow[0]=1;
for(int i=1;i<1<<N;i++) lg2[i]=lg2[i>>1]+1;
for(int j=1;j<1<<N;j++) Cnt[j]=Cnt[j>>1]+(j&1);
for(int i=1;i<=m;i++) Pow[i]=1ll*Pow[i-1]*2%mod;
f[0]=1;
for(int S=1;S<1<<n;S++){
// printf("S=%d\n",S);
for(int R=S&(S-1);;R=S&(R-1)){
int T=S-R,lb=lowbit(T);
//我们这里枚举 R=S-T
//从大到小枚举 R,就是从小到大枚举 T。
g[T]=g[T-lb]-Cnt[Pre[lg2[lb]]&T]+Cnt[Next[lg2[lb]]&R];
// printf("g[%d]=g[%d]-Cnt[Pre[%d]&%d]+Cnt[Next[%d]&%d]\n",T,T-lowbit(T),lg2[lowbit(T)]+1,T,lg2[lowbit(T)]+1,R);
// printf("%d=%d-%d+%d\n",g[T],g[T-lowbit(T)],Cnt[Pre[lg2[lowbit(T)]]&T],Cnt[Next[lg2[lowbit(T)]]&R]);
// printf("g[%d]=%d\n",T,g[T]);
f[S]=(f[S]+1ll*sgn(Cnt[T]-1)*Pow[g[T]]*f[R]%mod)%mod;
if(!R) break;
}
// printf("f[%d]=%d\n",S,f[S]);
}
printf("%lld\n",((Pow[m]-f[(1<<n)-1])%mod+mod)%mod);
return 0;
}