Skip to content

20231010 B 组模拟赛 题解

前言

这场我记得 T1 没看到“保证边在交点以外的任何地⽅不相交”这句话(其实是看到了,但是没看懂这句话是啥意思),然后 T4 是原题但是赛时没写出来,记得当时好像被初三老哥薄纱了。

然后这场四道都是原题,一看就是 h 先生的模拟赛。

密码是通用密码

T1

首先可以 $O(n)$ DFS 一遍给能从东侧到达的所有点染色,然后就只关注这些染了色的点了,把其它点全删掉,下文默认所有点都能从东侧到达。

注意这句话:

保证边在结点以外的任何地⽅不相交

这是什么意思?我们画一张图:

图示 1

假如黑色点和绿色边是已知的,然后存在一条路径从左侧红色点连到右侧红色点(橙色虚线)。由于边不相交,这就说明橙色路径中必定不和绿色路径相交,也就是必定经过黑色点。

图示 2

比如可能是这样的。

这说明什么?若存在两条路径 $u\to x,v\to y$ 相交,那么 $u$ 也能到达 $y$,$v$ 也能到达 $x$。那么考虑再进一步,容易发现,东侧点某个点 $u$ 必定能到达西侧的一整个连续的区间(或者无法到达西侧)。

那么就简单了,首先由于我们只关注连通性,可以缩掉强连通分量,然后就是一个 DAG 了,那么我们可以按反图的拓扑序 DP,维护每个强连通分量能到达的西侧最高点和最低点,然后把编号相减即可。具体转移很简单,不做赘述。

貌似有不用缩点的办法,但是我不太懂。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
using pii=pair<int,int>;
#define fi first
#define se second
#define mkp make_pair
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int N=3e5+5,inf=0x3f3f3f3f;
int dp[2][N],n,m,a,b,x[N],y[N],rd[N],ans[N];
int mp[N];
struct edge{
    int u,v;
};
vector<edge> sve;
vector<int> e[N],re[N],est[N];
int dfn[N],low[N],Tm,ist[N],csc,blg[N];
stack<int> stk;
queue<int> q;
vector<pii> eseq;
int vis[N];
void dfs1(int u){
    vis[u]=1;
    if(x[u]==a){
        eseq.push_back(mkp(y[u],u));
    }
    for(auto i:e[u]){
        if(vis[i]) continue;
        dfs1(i);
    }
}
void Tarjan(int j){
    low[j]=dfn[j]=++Tm;
    ist[j]=1;stk.push(j);
    for(auto i:e[j]){
        if(!vis[i]) continue;
        if(!dfn[i]){
            Tarjan(i);
            low[j]=min(low[j],low[i]);
        }else if(ist[i]){
            low[j]=min(low[j],dfn[i]);
        }       
    }
    if(dfn[j]==low[j]){
        ++csc;
        while(stk.top()!=j){
            int u=stk.top();
            blg[u]=csc;
            ist[u]=0;
            if(x[u]==a){
                dp[0][csc]=min(dp[0][csc],mp[u]);
                dp[1][csc]=max(dp[1][csc],mp[u]);
            }
            if(x[u]==0){
                est[csc].push_back(u);
            }
            stk.pop();
        }
        blg[j]=csc;
        ist[j]=0;
        if(x[j]==a){
            dp[0][csc]=min(dp[0][csc],mp[j]);
            dp[1][csc]=max(dp[1][csc],mp[j]);
        }
        if(x[j]==0){
            est[csc].push_back(j);
        }
        stk.pop();
    }
}
signed main(){
    n=read();m=read();a=read();b=read();
    mem(dp[0],0x3f);
    forup(i,1,n){
        x[i]=read();y[i]=read();
    }
    forup(i,1,m){
        int u=read(),v=read(),k=read();
        e[u].push_back(v);
        sve.push_back(edge{u,v});
        if(k==2){
            e[v].push_back(u);
        }
    }
    forup(i,1,n){
        if(x[i]==0){
            dfs1(i);
        }
    }
    sort(eseq.begin(),eseq.end());
    forup(i,0,eseq.size()-1){
        mp[eseq[i].se]=i+1;
    }
    forup(i,1,n){
        if(vis[i]&&!dfn[i]) Tarjan(i);
    }
    for(auto i:sve){
        if(!vis[i.u]||!vis[i.v]) continue;
        int u=blg[i.u],v=blg[i.v];
        if(u==v) continue;
        re[v].push_back(u);
    }
    forup(i,1,csc){
        sort(re[i].begin(),re[i].end());
        re[i].erase(unique(re[i].begin(),re[i].end()),re[i].end());
        for(auto j:re[i]){
            rd[j]++;
        }
    }
    while(q.size()) q.pop();
    forup(i,1,csc){
        if(rd[i]==0) q.push(i);
    }
    while(q.size()){
        int u=q.front();q.pop();
        for(auto i:re[u]){
            rd[i]--;
            if(rd[i]==0) q.push(i);
            dp[0][i]=min(dp[0][i],dp[0][u]);
            dp[1][i]=max(dp[1][i],dp[1][u]);
        }
    }
    forup(i,1,csc){
        for(auto j:est[i]){
            ans[j]=max(0,dp[1][i]-dp[0][i]+1);
        }
    }
    vector<pii> res;
    forup(i,1,n){
        if(x[i]==0){
            res.push_back(mkp(y[i],ans[i]));
        }
    }
    sort(res.begin(),res.end(),greater<pii>());
    for(auto i:res){
        printf("%d\n",i.se);
    }
}

T2

期望题(还是离散型的),赛时直接跳了。但貌似暴力很好打的样子,痛失暴力分。

首先注意提示中的这句话:

对于 $n$ 个 $[0,1]$ 之间的随机变量 $x_1,x_2,\dots,x_n$,第 $k$ ⼩的那个的期望值是 $\frac{k}{n+1}$。

他给这个提示肯定是有用的,容易发现我们只要求出最小生成树上最大的那条边的排名取到 $k\in[1,m]$ 的概率分别是多少,然后乘以贡献相加即可。

那么假如我们按边权从小到大加边,当整张图第一次联通的时候,正在加的这条边就会产生贡献。形象化的,把恰好加到第 $n$ 条边时整张图首次联通的概率记作 $P(x=n)$,那么答案就是 $\sum_{k=0}^m\frac{k}{m+1}P(x=k)=\frac{1}{m+1}\sum_{k=0}^mkP(x=k)$。套路地,转化为 $\frac{1}{m+1}\sum_{k+0}^mP(x>k)$,其中 $P(x>k)$ 表示加到 $k$ 条边时整张图仍不连通的概率。

由于 $n$ 很小,考虑状压,这里开始就非常巧妙了。设 $f_{i,msk}$ 表示加入了 $i$ 条边,每条边的左右端点都在 $msk$ 中,使 $msk$ 不连通的方案数,$g_{i,msk}$,表示连通的方案数。为方便叙述,设 $h(msk)$ 表示左右端点都在 $msk$ 内的边的总数,易得(?)转移方程:

$$f_{i,msk}=\sum_{S\subsetneq msk\land p\in S}\sum_{j=0}^ig_{j,S}\dbinom{h(msk-S)}{i-j}$$

其中 $p$ 是随意钦定的一个点,满足 $p\in msk$。

首先,上面的意思是把 $msk$ 分成 $S,msk-S$ 两个集合,然后钦定两个集合之间不连边,然后 $S$ 内部用 $j$ 条边连通,其余随意连边。

至于为什么要钦定一个 $p$,容易发现 $msk-S$ 是有可能连通的,如果不钦定会在 $j'=i-j$ 时将当前情况重复计算,如果钦定连通的那一半包含某个点,由于每个点必然属于其中一边,那么就会自动去掉重复的情况。而在每种情况下,钦定的 $p$ 必然属于某个连通块,而 $S$ 显然能取遍 $p$ 可能所属的所有连通块,这样就不重不漏了。

那么如何求 $g$ 呢?这其实非常简单,因为显然 $f_{i,msk}+g_{i,msk}=\dbinom{h(msk)}{i}$,$g$ 可以直接计算。于是就做完了,复杂度 $O(3^nm^2)$。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(i64 i=(s);i<=(e);i++)
#define fordown(i,s,e) for(i64 i=(s);i>=(e);i--)
using namespace std;
using i64=long long;
#define gc getchar()
inline i64 read(){
    i64 x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const i64 N=10,M=55;
i64 n,m,al;
i64 sz[1<<N],f[1<<N][M],g[1<<N][M];
i64 C[M][M];
double ans;
signed main(){
    n=read();m=read();
    al=(1<<n)-1;
    forup(i,1,m){
        i64 u=read()-1,v=read()-1;
        sz[(1<<u)|(1<<v)]++;
    }
    forup(i,0,m){
        C[i][0]=1;
        forup(j,1,i){
            C[i][j]=C[i-1][j-1]+C[i-1][j];
        }
    }
    forup(i,0,n-1){
        forup(msk,0,al){
            if(msk&(1<<i)){
                sz[msk]+=sz[msk^(1<<i)];
            }
        }
    }
    forup(i,0,al){
        if(i==(i&-i)){
            f[i][0]=0;
            g[i][0]=1;
        }else{
            f[i][0]=1;
            g[i][0]=0;
        }
    }
    forup(msk,1,al){
        i64 p=msk&-msk;
        forup(i,1,m){
            if(i>sz[msk]){
                g[msk][i]=f[msk][i]=0;
                continue;
            }
            for(i64 ss=msk&(msk-1);ss;ss=(ss-1)&msk){
                if(!(ss&p)) continue;
                forup(j,0,i){
                    f[msk][i]+=g[ss][j]*C[sz[msk^ss]][i-j];
                }
            }
            g[msk][i]=C[sz[msk]][i]-f[msk][i];
        }
    }
    forup(i,0,m){
        ans+=1.0*f[al][i]/C[m][i];
    }
    printf("%.6lf\n",ans/(m+1));
}

T3

挺板的数据结构题,但是赛后没看题解改出来了,中间大部分维护思路是在研学的时候想的,自己独立发明了线段树维护历史总和,感觉自己屌爆了。

首先考虑最大值怎么维护,最小值类似。

我们可以单调栈求出 $i$ 左侧最近的大于 $a_i$ 的位置 $L_i$,右侧第一个 $R_i$,那么 $i$ 在 $l\in(L_i,i],r\in [i,R_i)$ 的区间 $[l,r]$ 中会产生贡献,如果把区间按 $l$ 为纵坐标 $r$ 为横坐标放在矩阵上就是一个矩形,可以扫描线维护。

那么题意就转化成了有若干个三元组 $((x_1,y_1),(x_2,y_2),v)$,表示两点围成的矩形中所有点 $a_{i,j}$ 的权值都加 $v$,然后查询某矩形内所有点权值和。

那么可以把修改和查询都拆成两条垂直线段 $(x_1,y_1)-(x_1,y_2),(x_2,y_1)-(x_2,y_2)$,然后扫描线维护区间历史总和(即扫描到第 $i$ 列时 $v_i=\sum_{j=1}^ia_{i,j}$),然后前缀和减一减就完事了。

那么考虑如何维护,首先我们每个区间维护的东西形如这样:

图示 3

其中不同颜色的交界表示一个修改操作。

容易发现在上一次修改之前(不包含修改的那一刻)的历史总和是固定,后面不会再改变的,但是最后的橙色那段长度是随时间递增的。

那么容易想到对每个区间维护三个值:上一次修改之前的总和 $hsum$,上一次修改的时间 $modi$,当前值(即橙色部分每个切片的值) $sum$,然后查询时下传当前的时间 $T$,区间历史总和就是 $hsum+(T-modi+1)\times sum$。

接下来考虑如何修改。首先考虑合并怎么写,其实很好想,上次修改的时间是两个儿子取较大值,那么 $hsum$ 就要加上另一边在差的这段时间的贡献,可以自己想一想。

接下来是难点,如何下传懒标记(因为你要区间修改嘛)。如果你稍微熟悉一点线段树,就会发现懒标记是一段时间的积累。也就是说,懒标记也要维护一个形如上面那条色彩斑斓的东西的玩意。

另外你也容易发现区间加的懒标记维护的是一段时间的增量,那么我们维护四个值:懒标记的开始时间 $stmark$,懒标记的结束时间 $edmark$,懒标记总增量 $smark$,增量从开始到结束的总和 $mark$。然后下传懒标记时,对区间历史总和的贡献就是 $(smark+sum\times (ed-modi))\times len$,其中 $len$ 是区间长度。

至于懒标记下传给懒标记,仿照这个即可,不多做赘述了。

然后我突然发现其实不需要维护懒标记开始时间,鉴定为研学的时候没纸没笔脑子秀逗导致的,但是代码懒得改了。

复杂度 $O(n\log n)$,常数不小。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(i64 i=(s);i<=(e);i++)
#define fordown(i,s,e) for(i64 i=(s);i>=(e);i--)
using namespace std;
using i64=long long;
#define gc getchar()
inline i64 read(){
    i64 x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const i64 N=1e5+5,n=1e5,mod=1e9;
i64 q,mnl[N],mxl[N],mnr[N],mxr[N];
i64 ans[N],a[N],m1,m2;
struct Node{
    i64 l,r,pos,v;
};
struct SegTree{
    #define mid ((l+r)>>1)
    #define ls id<<1
    #define rs id<<1|1
    #define lson l,mid,ls
    #define rson mid+1,r,rs
    private:
        i64 querysum[N<<2],modi[N<<2],hsum[N<<2];
        i64 stmark[N<<2],edmark[N<<2],smark[N<<2],mark[N<<2];
        void _PushUp(i64 id){
            modi[id]=max(modi[ls],modi[rs]);
            querysum[id]=querysum[ls]+querysum[rs];
            hsum[id]=hsum[ls]+hsum[rs]+querysum[rs]*(modi[id]-modi[rs])+querysum[ls]*(modi[id]-modi[ls]);
        }
        void _Modify(i64 id,i64 len,i64 Tm,i64 val){
            hsum[id]+=querysum[id]*(Tm-modi[id]);
            querysum[id]+=val*len;
            modi[id]=Tm;
            if(stmark[id]==0){
                smark[id]=0;
                mark[id]=val;
                stmark[id]=edmark[id]=Tm;
            }else{
                smark[id]+=mark[id]*(Tm-edmark[id]);
                mark[id]+=val;
                edmark[id]=Tm;
            }
        }
        void _Mark_Modi(i64 id,i64 len,i64 st,i64 ed,i64 sum,i64 val){
            hsum[id]+=querysum[id]*(ed-modi[id]);
            hsum[id]+=sum*len;
            querysum[id]+=val*len;
            modi[id]=ed;
            if(stmark[id]==0){
                smark[id]=sum;
                mark[id]=val;
                stmark[id]=st;
                edmark[id]=ed;
            }else{
                smark[id]+=mark[id]*(ed-edmark[id]);
                smark[id]+=sum;
                mark[id]+=val;
                edmark[id]=ed;
            }
        }
        void _PushDown(i64 id,i64 len){
            _Mark_Modi(ls,((len+1)>>1),stmark[id],edmark[id],smark[id],mark[id]);
            _Mark_Modi(rs,len>>1,stmark[id],edmark[id],smark[id],mark[id]);
            mark[id]=stmark[id]=edmark[id]=smark[id]=0;
        }
    public:
        void Update(i64 L,i64 R,i64 Tm,i64 X,i64 l=1,i64 r=n,i64 id=1){
            if(L<=l&&r<=R){
                _Modify(id,r-l+1,Tm,X);
                return;
            }
            if(stmark[id]!=0) _PushDown(id,r-l+1);
            if(L<=mid) Update(L,R,Tm,X,lson);
            if(mid< R) Update(L,R,Tm,X,rson);
            _PushUp(id);
        }
        i64 Query(i64 L,i64 R,i64 Tm,i64 l=1,i64 r=n,i64 id=1){
            if(L<=l&&r<=R){
                return hsum[id]+querysum[id]*(Tm-modi[id]+1);
            }
            if(stmark[id]!=0) _PushDown(id,r-l+1);
            i64 res=0;
            if(L<=mid) res+=Query(L,R,Tm,lson);
            if(mid< R) res+=Query(L,R,Tm,rson);
            return res;
        }
}mt;
vector<Node> op[N],que[N];
void init(){
    m1=m2=1;
    forup(i,1,n){
        m1=m1*1023%mod;
        m2=m2*1025%mod;
        a[i]=m1^m2;
    }
    stack<i64> mns,mxs;
    mns.push(0);mxs.push(0);
    forup(i,1,n){
        while(mns.top()!=0&&a[mns.top()]>=a[i]) mns.pop();
        mnl[i]=mns.top()+1;
        mns.push(i);
        while(mxs.top()!=0&&a[mxs.top()]<=a[i]) mxs.pop();
        mxl[i]=mxs.top()+1;
        mxs.push(i);
    }
    while(mns.size()) mns.pop();
    while(mxs.size()) mxs.pop();
    mns.push(n+1);mxs.push(n+1);
    fordown(i,n,1){
        while(mns.top()!=n+1&&a[mns.top()]>a[i]) mns.pop();
        mnr[i]=mns.top()-1;
        mns.push(i);
        while(mxs.top()!=n+1&&a[mxs.top()]<a[i]) mxs.pop();
        mxr[i]=mxs.top()-1;
        mxs.push(i);
    }
    forup(i,1,n){
        op[mnl[i]].push_back(Node{i,mnr[i],0,-a[i]});
        op[i+1].push_back(Node{i,mnr[i],0,a[i]});
        op[mxl[i]].push_back(Node{i,mxr[i],0,a[i]});
        op[i+1].push_back(Node{i,mxr[i],0,-a[i]});
    }
}
signed main(){
    init();
    vector<i64> lsh;
    forup(i,1,10){
        lsh.push_back(a[i]);
    }
    sort(lsh.begin(),lsh.end());
    lsh.erase(unique(lsh.begin(),lsh.end()),lsh.end());
    q=read();
    forup(i,1,q){
        i64 l1=read(),r1=read(),l2=read(),r2=read();
        que[l1-1].push_back(Node{l2,r2,i,-1});
        que[r1].push_back(Node{l2,r2,i,1});
    }
    forup(i,1,n){
        for(auto j:op[i]){
            i64 l=j.l,r=j.r,v=j.v;
            mt.Update(l,r,i,v);
        }
        for(auto j:que[i]){
            i64 l=j.l,r=j.r,v=j.v,pos=j.pos;
            ans[pos]+=mt.Query(l,r,i)*v;
        }
    }
    forup(i,1,q){
        printf("%lld\n",ans[i]);
    }
}

T4

首先,题给你的是个内向基环森林,这点后面会用到。

考虑如何构造,容易发现加的所有边都从点 $1$ 指出必然是不劣的,且基环树的叶子结点必须引一条边。

那么每次找到最深的距离大于 $k$/无法到达(但不能在环上)的点连边,容易发现你连得每条边都是必要的,即这样必定是最优解。

接下来就只有环没处理了,那么问题就转化成了一个环上有若干个黑点与若干个白点,然后你一次操作可以把从指定点 $i$ 开始往后连续的 $k-1$ 个点全部染黑,问把环全染黑的最小操作数。

这个有非常巧妙的贪心。容易发现连续 $k-1$ 个白点(指的不是紧挨着,两个连续的白点可能越过多个黑点),中必定至少有一个要操作一次(不然就涂不完了)。而你涂色操作可以每次跳 $k$ 个 $O(\frac{n}{k})$ 算出一个环至少要操作多少次才完成,那么复杂度就是 $O(k\times \frac{n}{k})=O(n)$!那么我们就能愉快地通过此题了(可能有一些操作实现比较复杂)。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int N=5e5+5,inf=0x3f3f3f3f;
int n,m,co[N],inc[N],ans;
int to[N],rd[N],dis[N];
int dfn[N],low[N],Tm,ist[N];
stack<int> stk;
vector<vector<int> > cir;
void Tarjan(int x){
    low[x]=dfn[x]=++Tm;
    ist[x]=1;stk.push(x);
    if(!dfn[to[x]]){
        Tarjan(to[x]);
        low[x]=min(low[x],low[to[x]]);
    }else if(ist[to[x]]){
        low[x]=min(low[x],dfn[to[x]]);
    }
    if(dfn[x]==low[x]){
        vector<int> vec;
        while(stk.top()!=x){
            vec.push_back(stk.top());
            inc[stk.top()]=1;
            ist[stk.top()]=0;
            stk.pop();
        }
        vec.push_back(x);
        inc[x]=1;
        ist[x]=0;
        stk.pop();
        if(vec.size()>1){
            reverse(vec.begin(),vec.end());
            cir.push_back(vec);
        }else{
            inc[x]=0;
        }
    }
}
queue<int> q;
signed main(){
    n=read();m=read();
    forup(i,1,n){
        int u=read(),v=read();
        to[u]=v;
        ++rd[v];
    }
    mem(dis,0x3f);
    dis[1]=0;
    q.push(1);
    forup(i,1,n){
        if(!dfn[i]){
            Tarjan(i);
        }
        if(i!=1&&rd[i]==0){
            q.push(i);
            ++ans;
            dis[i]=1;
        }
    }
    while(q.size()){
        int u=q.front();q.pop();
        if(dis[u]>m){
            if(inc[u]) continue;
            ++ans;dis[u]=1;
        }
        co[u]=1;
        rd[to[u]]--;
        if(to[u]==1) continue;
        if(rd[to[u]]==0) q.push(to[u]);
        dis[to[u]]=min(dis[to[u]],dis[u]+1);
    }
    for(auto vec:cir){
        int sz=vec.size();
        vector<int> v1,nxt;
        int nd=inf;
        for(auto i:vec){
            nd=min(nd+1,dis[i]);
            if(nd<=m) v1.push_back(1);
            else v1.push_back(0);
        }
        forup(i,0,sz-1){
            v1.push_back(v1[i]);
        }
        nxt.resize(sz*2);
        int nw=inf,cnt=0,res=inf;
        fordown(i,sz*2-1,0){
            if(!v1[i]) nw=i;
            nxt[i]=nw;
        }
        int i=nxt[0];
        while(i<sz){
            int l=i,rr=0;
            ++cnt;
            while(l<i+sz){
                ++rr;
                if(l+m>=i+sz) break;
                l=nxt[l+m];
            }
            i=nxt[i+1];
            res=min(res,rr);
        }
        if(res==inf) res=0;
        ans+=res;
    }
    printf("%d\n",ans);
}

Comments