JOISC2020题解

发布时间 2023-12-18 15:41:55作者: DaiRuiChen007

\(\text{By DaiRuiChen007}\)


Contest Link


A. Building 4

Problem Link

题目大意

\(2n\) 个数对 \((a_i,b_i)\),构造一个非降序列 \(c_i\) 满足 \(\forall 1\le i\le n,c_i\in\{a_i,b_i\}\),且 \(c_i=a_i\) 的位置恰好有 \(n\) 个。

数据范围:\(n\le 5\times 10^5\)

思路分析

考虑 dp,最朴素的设计就是令 \(dp_{i,j,0/1}\) 表示 \(c_1\sim c_i\) 中选了 \(j\)\(a_i\),且上一个是 \(a_i/b_i\) 是否可行。

打表可以发现:每个 \(dp_{i,*,0/1}\) 中合法的 \(j\) 都是连续的一段,这样就只要记录左右端点 \(\mathcal O(1)\) 转移了。

输出方案直接倒序还原即可。

时间复杂度 \(\mathcal O(n)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5,inf=1e9;
struct Info {
    int l,r;
    Info(int x=inf,int y=-inf): l(x),r(y) {}
    inline friend Info operator +(const Info &u,const Info &v) { return Info(min(u.l,v.l),max(u.r,v.r)); }
    inline bool in(int x) { return l<=x&&x<=r; } 
} dp[MAXN][2];
int n,a[MAXN],b[MAXN];
char o[MAXN];
signed main() {
    scanf("%d",&n),n<<=1;
    for(int i=1;i<=n;++i) scanf("%d",&a[i]);
    for(int i=1;i<=n;++i) scanf("%d",&b[i]);
    dp[1][0]={0,0},dp[1][1]={1,1};
    for(int i=2;i<=n;++i) {
        if(a[i-1]<=a[i]) dp[i][0]=dp[i][0]+dp[i-1][0];
        if(a[i-1]<=b[i]) dp[i][1]=dp[i][1]+dp[i-1][0];
        if(b[i-1]<=b[i]) dp[i][1]=dp[i][1]+dp[i-1][1];
        if(b[i-1]<=a[i]) dp[i][0]=dp[i][0]+dp[i-1][1];
        ++dp[i][1].l,++dp[i][1].r;
    }
    if(dp[n][0].in(n/2)||dp[n][1].in(n/2)) {
        for(int i=n,k=0,s;i;--i) {
            if((i==n||a[i]<=(s?b[i+1]:a[i+1]))&&dp[i][0].in(n/2-k)) s=0;
            else s=1,++k;
            o[i]="AB"[s];
        }
        for(int i=1;i<=n;++i) printf("%c",o[i]);
        puts("");
    } else puts("-1");
    return 0;
}

B. Hamburg Steak

Problem Link

题目大意

\(n\) 个矩形,在平面上选定 \(k\) 个点使每个矩形内至少有一个被选定的点。

数据范围:\(n\le 5\times 10^5\)\(k\le 4\),保证有解。

思路分析

先考虑 \(k\le3\) 的情况。

先求出所有矩形的交,如果非空,那么任取一个都可以。

如果一边非空(最大左端点 \(\le\) 最小右端点),可以根据调整法证明点一定放在中间,因此可以降成一维的情况,那么一维的情况直接贪心按右端点放就行。

如果两边都为空(最大左端点 \(>\) 最小右端点,最大上端点 \(>\) 最小下端点),显然每个边界上至少要有一个点,但又由于 \(k\le 3\),那么至少要有一个交点上有点,枚举选了哪个端点然后递归处理即可。

\(k=4\) 的时候,若依然有点是交点,则也进行刚才同样的爆搜处理。

接下来我们只要处理四个边界上分别恰有一个点的情况,能够证明四个点一定在四条边界构成的矩形上。

对坐标离散化,然后建立 2-SAT 模型,如 \(L_i\) 表示左边界上 \(i\) 是不是被选的点。

考虑分类讨论每个矩形和几条边界有交:

  • 若有 \(3\)\(4\) 条边界有交,显然至少有一条边界被完全包含,则这个矩形一定满足。
  • 若和 \(1\) 条边界有交,显然直接更新对应的可能被选区间。
  • 若不和区间有交,至少要一个点放在边界矩形内部,那么这种情况一定在刚刚选拐点的过程中处理过。
  • 若和 \(2\) 条边界有交,那么限制形如 \(X_{l_1,r_1}\lor Y_{l_2,r_2}=1\),其中 \(X,Y\) 是边界,\([l_1,r_1],[l_2,r_2]\) 表示对应边界上的一个子区间,但限制关系,考虑前缀和优化,注意到 \(X_{l,r}=(X_{1,r}\land\lnot X_{1,l-1})\),因此原限制条件可以拆成 \(\mathcal O(1)\) 组前缀变量上的限制关系,这样限制关系数就可以接受。

时间复杂度:\(\mathcal O(4^kn+n\log n)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,inf=1e9;
struct Rect { int lox,hix,loy,hiy; };
inline Rect merge(Rect a,Rect b) {
    return {max(a.lox,b.lox),min(a.hix,b.hix),max(a.loy,b.loy),min(a.hiy,b.hiy)};
}
typedef array<int,2> Pair;
inline vector<Pair> dfs(const vector<Rect> &now,int k) {
    if(!k) return vector<Pair>(0);
    Rect inter{1,inf,1,inf};
    for(auto re:now) inter=merge(inter,re);
    int L=inter.lox,R=inter.hix,D=inter.loy,U=inter.hiy;
    if(L<=R&&D<=U) return vector<Pair>(k,{L,D});
    if(L<=R) {
        vector <Pair> sec,ans;
        for(auto re:now) sec.push_back({re.loy,re.hiy});
        sort(sec.begin(),sec.end(),[&](auto I,auto J){ return I[1]<J[1]; });
        int lst=0;
        for(auto s:sec) if(lst<s[0]) ans.push_back({L,lst=s[1]});
        if((int)ans.size()>k) ans.clear();
        else while((int)ans.size()<k) ans.push_back({inf,inf});
        return ans;
    }
    if(D<=U) {
        vector <Pair> sec,ans;
        for(auto re:now) sec.push_back({re.lox,re.hix});
        sort(sec.begin(),sec.end(),[&](auto I,auto J){ return I[1]<J[1]; });
        int lst=0;
        for(auto s:sec) if(lst<s[0]) ans.push_back({lst=s[1],U});
        if((int)ans.size()>k) ans.clear();
        else while((int)ans.size()<k) ans.push_back({inf,inf});
        return ans;
    }
    for(int o:{0,1,2,3}) {
        int x=(o&1)?L:R,y=(o&2)?U:D;
        vector <Rect> nxt;
        for(auto Re:now) if(x<Re.lox||x>Re.hix||y<Re.loy||y>Re.hiy) nxt.push_back(Re);
        vector <Pair> ans=dfs(nxt,k-1);
        if(ans.empty()) continue;
        ans.push_back({x,y});
        return ans;
    }
    return vector<Pair>(0);
}
int U[MAXN*2][2],D[MAXN*2][2],L[MAXN*2][2],R[MAXN*2][2];
int vx[MAXN*2],vy[MAXN*2];
vector <int> G[MAXN*16];
inline void link(int u,int v) { G[u].push_back(v); }
int dfn[MAXN*16],low[MAXN*16],dcnt;
int stk[MAXN*16],tp;
bool ins[MAXN*16];
int bel[MAXN*16],scnt;
inline void tarjan(int u) {
    dfn[u]=low[u]=++dcnt;
    ins[stk[++tp]=u]=true;
    for(int v:G[u]) {
        if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
        else if(ins[v]) low[u]=min(low[u],dfn[v]);
    }
    if(low[u]==dfn[u]) {
        int k; ++scnt;
        do {
            ins[k=stk[tp--]]=false;
            bel[k]=scnt;
        } while(k^u);
    }
}
signed main() {
    int n,k;
    scanf("%d%d",&n,&k);
    vector <Rect> a(n);
    for(int i=0;i<n;++i) scanf("%d%d%d%d",&a[i].lox,&a[i].loy,&a[i].hix,&a[i].hiy);
    auto ans=dfs(a,k);
    if(!ans.empty()) {
        for(auto I:ans) printf("%d %d\n",I[0],I[1]);
        return 0;
    }
    for(auto re:a) {
        vx[++vx[0]]=re.lox,vx[++vx[0]]=re.hix;
        vy[++vy[0]]=re.loy,vy[++vy[0]]=re.hiy;
    }
    sort(vx+1,vx+vx[0]+1),vx[0]=unique(vx+1,vx+vx[0]+1)-vx-1;
    sort(vy+1,vy+vy[0]+1),vy[0]=unique(vy+1,vy+vy[0]+1)-vy-1;
    for(auto &re:a) {
        re.lox=lower_bound(vx+1,vx+vx[0]+1,re.lox)-vx;
        re.hix=lower_bound(vx+1,vx+vx[0]+1,re.hix)-vx;
        re.loy=lower_bound(vy+1,vy+vy[0]+1,re.loy)-vy;
        re.hiy=lower_bound(vy+1,vy+vy[0]+1,re.hiy)-vy;
    }
    Rect inter{1,inf,1,inf};
    for(auto re:a) inter=merge(inter,re);
    int l=inter.hix,r=inter.lox,d=inter.hiy,u=inter.loy;
    int vcnt=0;
    for(int i=l;i<=r;++i) U[i][0]=++vcnt,D[i][0]=++vcnt;
    for(int i=d;i<=u;++i) L[i][0]=++vcnt,R[i][0]=++vcnt;
    for(int i=l;i<=r;++i) U[i][1]=++vcnt,D[i][1]=++vcnt;
    for(int i=d;i<=u;++i) L[i][1]=++vcnt,R[i][1]=++vcnt;
    for(int i=l;i<r;++i) {
        link(U[i][1],U[i+1][1]);
        link(U[i+1][0],U[i][0]);
        link(D[i][1],D[i+1][1]);
        link(D[i+1][0],D[i][0]);
    }
    link(D[r][0],D[r][1]);
    link(U[r][0],U[r][1]);
    for(int i=d;i<u;++i) {
        link(L[i][1],L[i+1][1]);
        link(L[i+1][0],L[i][0]);
        link(R[i][1],R[i+1][1]);
        link(R[i+1][0],R[i][0]);
    }
    link(L[u][0],L[u][1]);
    link(R[u][0],R[u][1]);
    for(auto re:a) {
        int tl,tr,tu,td;
        bool il=(tl=re.lox)<=l,ir=(tr=re.hix)>=r;
        bool id=(td=re.loy)<=d,iu=(tu=re.hiy)>=u;
        int sum=il+ir+id+iu;
        if(sum>=3) continue;
        if(sum==1) {
            if(il) {
                link(L[td-1][1],L[td-1][0]);
                link(L[tu][0],L[tu][1]);
            }
            if(ir) {
                link(R[td-1][1],R[td-1][0]);
                link(R[tu][0],R[tu][1]);
            }
            if(id) {
                link(D[tl-1][1],D[tl-1][0]);
                link(D[tr][0],D[tr][1]);
            }
            if(iu) {
                link(U[tl-1][1],U[tl-1][0]);
                link(U[tr][0],U[tr][1]);
            }
        }
        if(sum==2) {
            if(il&&iu) {
                link(U[tr][0],L[td-1][0]);
                link(L[td-1][1],U[tr][1]);
            }
            if(il&&id) {
                link(L[tu][0],D[tr][1]);
                link(D[tr][0],L[tu][1]);
            }
            if(ir&&iu) {
                link(U[tl-1][1],R[td-1][0]);
                link(R[td-1][1],U[tl-1][0]);
            }
            if(ir&&id) {
                link(R[tu][0],D[tl-1][0]);
                link(D[tl-1][1],R[tu][1]);
            }
            if(il&&ir) {
                link(L[td-1][1],R[td-1][0]);
                link(L[td-1][1],R[tu][1]);
                link(L[tu][0],R[td-1][0]);
                link(L[tu][0],R[tu][1]);
                link(R[td-1][1],L[td-1][0]);
                link(R[td-1][1],L[tu][1]);
                link(R[tu][0],L[td-1][0]);
                link(R[tu][0],L[tu][1]);
            }
            if(id&&iu) {
                link(D[tl-1][1],U[tl-1][0]);
                link(D[tl-1][1],U[tr][1]);
                link(D[tr][0],U[tl-1][0]);
                link(D[tr][0],U[tr][1]);
                link(U[tl-1][1],D[tl-1][0]);
                link(U[tl-1][1],D[tr][1]);
                link(U[tr][0],D[tl-1][0]);
                link(U[tr][0],D[tr][1]);
            }
        }
    }
    for(int i=1;i<=vcnt;++i) if(!dfn[i]) tarjan(i);
    for(int i=d;i<=u;++i) {
        bool lst0=(i==d||bel[L[i-1][0]]<bel[L[i-1][1]]);
        bool now1=bel[L[i][0]]>bel[L[i][1]];
        if(lst0&&now1) printf("%d %d\n",vx[l],vy[i]);
    }
    for(int i=d;i<=u;++i) {
        bool lst0=(i==d||bel[R[i-1][0]]<bel[R[i-1][1]]);
        bool now1=bel[R[i][0]]>bel[R[i][1]];
        if(lst0&&now1) printf("%d %d\n",vx[r],vy[i]);
    }
    for(int i=l;i<=r;++i) {
        bool lst0=(i==l||bel[D[i-1][0]]<bel[D[i-1][1]]);
        bool now1=bel[D[i][0]]>bel[D[i][1]];
        if(lst0&&now1) printf("%d %d\n",vx[i],vy[d]);
    }
    for(int i=l;i<=r;++i) {
        bool lst0=(i==l||bel[U[i-1][0]]<bel[U[i-1][1]]);
        bool now1=bel[U[i][0]]>bel[U[i][1]];
        if(lst0&&now1) printf("%d %d\n",vx[i],vy[u]);
    }
    return 0;
}

C. Sweeping

Problem Link

题目大意

在平面直角坐标系上有一个等腰直角三角形坐标,其中两条直角边分别平行于 X 轴和 Y 轴,斜边连接 \((0,n)\)\((n,0)\)

区域中有 \(m\) 个点,要进行如下 \(q\) 次操作:

  • 插入一个点。
  • 查询一个点的坐标。
  • 给定 \(v\),把 \(y\le v\) 的点的横坐标与 \(n-v\)\(\max\)
  • 给定 \(v\),把 \(x\le v\) 的点的纵坐标与 \(n-v\)\(\max\)

数据范围:\(n\le 10^9,m\le 5\times 10^5,q\le 10^6\)

思路分析

考虑分治,每次处理一个矩形 \((0,0)\sim(k,n-k)\) 范围内的所有操作,显然处理完之后会剩下两个小三角形区域未处理,把区域内的操作处理出来递归即可。

对于插入操作和询问操作:判断对应点是否在矩形内部即可,不在就传到上侧或右侧三角形里递归,接下来的操作只考虑矩形内部的点。

对于一个向右推平的操作:

  • 如果 \(v<n-k\),那么该操作就会把若干 \(y\le v\) 的点推到右侧,用堆维护对应的点集,然后把横坐标设成 \(v+1\),并把所有点连同该操作传进右侧三角形。

  • 如果 \(v\ge n-k\),那么该操作就会把若干 \(x\le n-v\) 的点的横坐标设成 \(n-v\),可以用堆维护,但为了保证复杂度,我们对于这些横坐标相同的点用并查集合并起来,只留一个代表元插入堆中,这样就可以保证每次操作至多加入一个点入堆,且每个点只会被删除一次,单层中的操作次数为 \(\mathcal O(m+q)\) 级别。

    但此时对于一个向上推平出界到上侧三角形的操作,每次从堆中取出来的点实际上是一个代表元,我们要把这个代表元对应的所有点都从矩形中删除,因此我们要维护每个代表元实际的集合,由于每个点只会被删除一次,因此我们维护并查集合并关系对应的树结构,然后取出的时候在树上 BFS 一遍即可。

    特别地,\(v>n-k\) 时会对上侧矩形产生影响,需要传进上侧三角形。

向上推的操作也是同理,类似维护堆和并查集即可。

注意到任何一个操作始终只会单侧递归到上方三角形或右侧三角形,因此每层操作数总和为 \(\mathcal O((m+q)\log n)\)

时间复杂度 \(\mathcal O((m+q)\log n\log (m+q))\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1.5e6+5;
struct info { int op,x,y,id; };
struct cmpx { inline bool operator ()(const info &u,const info &v) { return u.x>v.x; } };
struct cmpy { inline bool operator ()(const info &u,const info &v) { return u.y>v.y; } };
struct DSU {
    int dsu[MAXN];
    inline int find(int x) { return dsu[x]==x?x:(dsu[x]=find(dsu[x])); }
    inline void merge(int x,int y) { dsu[find(x)]=find(y); }
}    dx,dy;
int n,px[MAXN],py[MAXN],ax[MAXN],ay[MAXN],out[MAXN],que[MAXN],hd,tl,vid[MAXN];
vector <int> gx[MAXN],gy[MAXN];
inline void solve(int l,int r,vector<info>&Q) {
    if(l>r) return ;
    if(l==r) {
        for(auto &q:Q) if(q.op==1) ax[q.id]=l,ay[q.id]=n-l;
        return ;
    }
    priority_queue <info,vector<info>,cmpx> qx;
    priority_queue <info,vector<info>,cmpy> qy;
    int k=(l+r)>>1;
    vector <info> nxt[2]; //0:up,1:right
    for(auto &q:Q) {
        if(q.op==1) {
            if(~out[q.x]) nxt[out[q.x]].push_back(q);
            else ax[q.id]=px[dx.find(q.x)],ay[q.id]=py[dy.find(q.x)];
        } 
        if(q.op==2) {
            if(q.x<n-k) {
                while(qy.size()&&qy.top().y<=q.x) {
                    int w=qy.top().id; qy.pop();
                    hd=1,tl=1,que[1]=w;
                    while(hd<=tl) {
                        int u=que[hd++];
                        if(out[u]==-1) nxt[out[u]=1].push_back({4,k+1,py[w],u});
                        for(int v:gy[u]) que[++tl]=v;
                        vector<int>().swap(gy[u]);
                    }
                }
                nxt[1].push_back(q);
            } else {
                hd=1,tl=0;
                while(qx.size()&&qx.top().x<=n-q.x) que[++tl]=qx.top().id,qx.pop();
                if(hd<=tl) {
                    int p=que[tl];
                    qx.push({4,px[p]=n-q.x,py[p],p});
                    for(int i=hd;i<tl;++i) gx[p].push_back(que[i]),dx.merge(que[i],p);
                }
                if(q.x>n-k) nxt[0].push_back(q);
            }
        }
        if(q.op==3) {
            if(q.x<k) {
                while(qx.size()&&qx.top().x<=q.x) {
                    int w=qx.top().id; qx.pop();
                    hd=1,tl=1,que[1]=w;
                    while(hd<=tl) {
                        int u=que[hd++];
                        if(out[u]==-1) nxt[out[u]=0].push_back({4,px[w],n-k+1,u});
                        for(int v:gx[u]) que[++tl]=v;
                        vector<int>().swap(gx[u]);
                    }
                }
                nxt[0].push_back(q);
            } else {
                hd=1,tl=0;
                while(qy.size()&&qy.top().y<=n-q.x) que[++tl]=qy.top().id,qy.pop();
                if(hd<=tl) {
                    int p=que[tl];
                    qy.push({4,px[p],py[p]=n-q.x,p});
                    for(int i=hd;i<tl;++i) gy[p].push_back(que[i]),dy.merge(que[i],p);
                }
                if(q.x>k) nxt[1].push_back(q);
            }
        }
        if(q.op==4) {
            int i=q.id;
            if(q.y>n-k) { nxt[out[i]=0].push_back(q); continue; }
            if(q.x>k) { nxt[out[i]=1].push_back(q); continue; }
            out[i]=-1,px[i]=q.x,py[i]=q.y,dx.dsu[i]=i,dy.dsu[i]=i;
            gx[i].clear(),gy[i].clear(),qx.push(q),qy.push(q);
        }
    }
    vector<info>().swap(Q);
    solve(l,k-1,nxt[0]),solve(k+1,r,nxt[1]);
}
signed main() {
    int m,q,vc;
    ios::sync_with_stdio(false);
    cin>>n>>m>>q,vc=m;
    vector <info> Q; vector <int> qry;
    for(int i=1,x,y;i<=m;++i) cin>>x>>y,Q.push_back({4,x,y,i}),vid[i]=i;
    for(int i=m+1,op,x,y;i<=m+q;++i) {
        cin>>op>>x;
        if(op==4) cin>>y,Q.push_back({op,x,y,i}),vid[++vc]=i;
        else if(op==1) Q.push_back({op,vid[x],0,i}),qry.push_back(i);
        else Q.push_back({op,x,0,i});
    }
    solve(0,n,Q);
    for(int i:qry) cout<<ax[i]<<" "<<ay[i]<<"\n";
    return 0;
}

D. Chameleon's Love

Problem Link

题目大意

\(2n\) 个点,\(n\) 个黑点 \(n\) 个白点,每个点有一个初始颜色 \(c_x\)

保证黑点的颜色和白点的颜色分别构成一个 \(1\sim n\) 的排列。

每个点有一个指向的点 \(t(x)\),满足 \(c_x\ne c_{t(x)}\)\(x,t(x)\) 不同为黑点或白点。

定义 \(d_x(S)\) 表示:\(t(x)\in S\)\(d_x(S)=c_{t(x)}\),否则 \(d_x(S)=c_x\)

每次你可以询问 \(S\),交互器会返回 \(\{d_x(S)|x\in S\}\) 中有多少个元素。

请在 \(2\times 10^4\) 次求出每个初始颜色相同的点对。

数据范围:\(n\le 500\)

思路分析

考虑询问 \(S=\{u,v\}\),观察发现如果 \(u=t(v)\)\(v=t(u)\)\(c_u=c_v\) 时答案为 \(1\)\(u=t(v)\)\(v=t(u)\) 或其他情况答案为 \(2\)

如果询问 \(\{u,v\}\) 答案为 \(1\),则连接 \(u,v\),此时一个点度数为 \(1/3\),度数为 \(1\) 那么连边的点就是答案。

如果度数为 \(3\),设邻域为 \(\{x,y,z\}\),考虑询问 \(\{u,x,y\}\),注意到当且仅当 \(c_u=c_y\)\(t_x=u\) 时(可交换 \(x,y\))答案为 \(1\)

因此我们能求出每个 \(u\) 的三条出边中哪个是 \(t_u\),然后就能得到所 \(c_u\) 同色的点,这里的操作次数为 \(4n\)

问题转成了如何求出一个点的邻域。

假如我们知道每个点是黑色还是白色,那么我们可以在其对面的一侧二分,每次考虑取出 \(u\) 对侧的一段前缀,由于同侧点内不会影响 \(d_x(S)\) 的值,因此询问答案不等于集合大小当且仅当该前缀里有 \(u\) 的邻域。

而观察该二分,我们依赖的性质仅仅是同侧点内没有连边关系互相影响,因此我们只要任意取出一个独立集都可以求解邻域。

显然我们建出来的图一定是二分图,将其黑白染色,对每种颜色的集合都二分求一次邻域即可。

询问次数 \(6n\log n+2n\le 38000\),可以通过本题。

时间复杂度 \(\mathcal O(n^2\log n)\)

代码呈现

#include<bits/stdc++.h>
#include"chameleon.h"
using namespace std;
const int MAXN=1005;
int col[MAXN],in[MAXN],out[MAXN];
bool vis[MAXN];
vector <int> G[MAXN],ver[2];
void dfs(int u,int c) {
    vis[u]=true,ver[col[u]=c].push_back(u);
    for(int v:G[u]) if(!vis[v]) dfs(v,c^1);
}
void Solve(int N) {
    for(int i=1;i<=2*N;++i) {
        fill(vis,vis+i,0),ver[0].clear(),ver[1].clear();
        for(int j=1;j<i;++j) if(!vis[j]) dfs(j,0);
        for(int c:{0,1}) {
            int x=0; auto it=ver[c].begin();
            vector <int> Q;
            for(int T:{0,1,2}) {
                int l=x,r=ver[c].size()-1,p=r+1;
                if(l>r) break;
                Q=vector<int>(it+l,it+r+1),Q.push_back(i);
                if(Query(Q)==(int)Q.size()) break;
                while(l<=r) {
                    int mid=(l+r)>>1;
                    Q=vector<int>(it+l,it+mid+1),Q.push_back(i);
                    if(Query(Q)<(int)Q.size()) p=mid,r=mid-1;
                    else l=mid+1;
                }
                G[i].push_back(ver[c][p]),G[ver[c][p]].push_back(i),x=p+1;
            }
        }
    }
    for(int u=1;u<=2*N;++u) if(G[u].size()==3) {
        int x=G[u][0],y=G[u][1],z=G[u][2];
        if(Query({u,x,y})==1) out[u]=z,in[z]=u;
        else if(Query({u,x,z})==1) out[u]=y,in[y]=u;
        else out[u]=x,in[x]=u;
    }
    memset(vis,false,sizeof(vis));
    for(int u=1;u<=2*N;++u) if(!vis[u]) {
        int v;
        if(G[u].size()==3) v=G[u][0]^G[u][1]^G[u][2]^in[u]^out[u];
        else v=G[u][0];
        Answer(u,v),vis[u]=vis[v]=true;
    }
}

E. Making Friends on Joitter is Fun

Problem Link

题目大意

\(n\) 个点,\(m\) 次操作加入一条有向边,然后按如下规则不断更新整个图:若 \(x\to u,u\leftrightarrow v\) 则连接 \(x\to v\)

数据范围:\(n\le 10^5,m\le 3\times 10^5\)

思路分析

容易发现双向连边关系具有传递性,因此每个点连向的一定是一个完整的双向边构成的连通块。

因此我们只要维护所有双向连边关系构成的连通块即可,对每个连通块维护入点和出点的集合,每次相当于合并两个集合。

使用启发式合并 \(x,y\),每次暴力更改大小小的一侧集合 \(y\),把所有端点指向 \(x\) 即可。

时间复杂度 \(\mathcal O(m\log n)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5;
unordered_set <int> in[MAXN],out[MAXN];
//in:all node link u, out: every dsu linked
int n,m,siz[MAXN],dsu[MAXN];
ll ans=0;
inline int find(int x) { return dsu[x]^x?(dsu[x]=find(dsu[x])):x; }
inline void link(int u,int v) {
    u=find(u),v=find(v); if(u==v) return ;
    if(in[u].size()+out[u].size()<in[v].size()+out[v].size()) swap(u,v);
    ans-=1ll*siz[u]*in[u].size()+1ll*siz[v]*in[v].size();
    vector <int> nxt;
    for(int x:out[v]) if(out[x].count(u)) nxt.push_back(x);
    for(int x:in[v]) {
        x=find(x);
        if(out[u].count(x)) nxt.push_back(x);
        out[x].erase(v),out[x].insert(u);
    } 
    dsu[v]=u,siz[u]+=siz[v];
    for(int x:in[v]) in[u].insert(x);
    for(int x:out[v]) out[u].insert(x);
    ans+=1ll*siz[u]*in[u].size();
    for(int x:nxt) link(u,x);
}
signed main() {
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i) siz[i]=1,dsu[i]=i,in[i].insert(i);
    for(int i=1,u,v;i<=m;++i) {
        scanf("%d%d",&u,&v);
        int x=find(u),y=find(v);
        if(x!=y) {
            if(!out[y].count(x)) {
                if(!in[y].count(u)) ans+=siz[y],in[y].insert(u),out[x].insert(y);
            } else link(u,v);
        }
        printf("%lld\n",ans);
    }
    return 0;
}

F. Ruins 3

Problem Link

题目大意

给定一个长度为 \(a_1\sim a_{2n}\),其中 \(1\sim n\) 每个数出现 \(2\) 次。

接下来会进行 \(n\) 次操作,每次操作同时考虑每个 \(i\)

  • 如果 \(a_{i+1}\sim a_{2n}\) 中有与 \(a_i\) 相同的元素,就令 \(a_i\gets a_i-1\)

已知最后哪 \(n\) 个位置非 \(0\),求原本的序列有多少种可能。

数据范围:\(n\le 600\)

思路分析

不妨钦定两个相同的高度属于不同的元素,最后再把答案除以 \(2^n\) 即可。

假如我们知道原序列 \(\{a_i\}\),考虑从后往前确定最终序列 \(\{b_i\}\):对于每个 \(a_i\)\(b_i\) 变成 \([1,a_i]\) 中最大的一个不在 \(b_{i+1}\sim b_{2n}\) 中出现的数,没有就变成 \(0\)

因此我们在 dp 的时候考虑只记录后缀的 \(\mathrm{mex}\),设 \(dp_{i,j}\) 表示后缀 \(a_{i}\sim a_{2n}\)\(\mathrm{mex}\)\(j+1\) 的方案数。

\(b_{i+1}\sim b_{2n}\)\(0\)\(s_0\) 个,非 \(0\) 元素有 \(s_1\) 个。

\(b_i=0\),说明 \(a_i\le j\),此时的 \(a_i\) 的选择范围为 \(2j\) 个数,但为了构成 \(\mathrm{mex}=j\) 花费了 \(j\) 个,之前的元素又花了 \(s_0\) 个,因此转移为 \(dp_{i,j}\gets (j-s_0)\times dp_{i+1,j}\)

\(b_i\ne 0\),若 \(a_i>j\) 那么我们先不考虑放置方案,等到 \(\mathrm{mex}\) 跨越 \(b_i\) 时再考虑。

否则说明 \(\mathrm{mex}\) 变大了,设之前的 \(\mathrm{mex}\)\(j-k\),那么 \(b_i=j-k\),而 \(a_i\)\(k+1\) 种选择(\(+1\) 是因为有两个 \(j-k\))。

先在 \(s_1-(j-k)\) 个未确定的非 \(0\) 位中选 \(k-1\) 个,这 \(k-1\) 个数的值可以看做一个独立问题,设转移系数为 \(f_{k-1}\),我们得到转移:

\[dp_{i,j}=dp_{i+1,j}+\sum_{k=1}^jdp_{i+1,j-k}\times(k+1)\times \binom{s_1-(j-k)}{k-1}\times f_{k-1} \]

根据定义,\(f_n\) 表示 \(1,1,2,2,\dots ,n,n\) 排成一个长度为 \(n\) 的序列,由题目中的的操作简化后可以得到 \(n\) 的排列。

考虑枚举 \(b_1=i\),那么 \(a_1\)\(n-i+2\) 种选法(\(+2\) 是因为有两个 \(i\)),而剩下的部分中 \(>i\)\(<i\) 两部分独立,组合数分配位置后变成两个子问题,最终得到:

\[f_n=\sum_{i=1}^n(n-i+2)\times \binom{n-1}{i-1}\times f_{n-i}\times f_{i-1} \]

预处理组合数暴力计算即可。

时间复杂度 \(\mathcal O(n^3)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=605,MOD=1e9+7,i2=(MOD+1)/2;
int n,p[MAXN],a[MAXN*2];
ll dp[MAXN*2][MAXN],f[MAXN],C[MAXN*2][MAXN*2];
signed main() {
    scanf("%d",&n);
    for(int i=0;i<=2*n;++i) for(int j=C[i][0]=1;j<=i;++j) {
        C[i][j]=(C[i-1][j]+C[i-1][j-1])%MOD;
    }
    for(int i=1;i<=n;++i) scanf("%d",&p[i]),a[p[i]]=1;
    f[0]=1;
    for(int i=1;i<=n;++i) {
        for(int j=1;j<=i;++j) {
            f[i]=(f[i]+(i-j+2)*C[i-1][j-1]%MOD*f[j-1]%MOD*f[i-j]%MOD)%MOD;
        }
    }
    dp[2*n+1][0]=1;
    for(int i=2*n,s0=0,s1=0;i>=1;--i) {
        if(a[i]) {
            for(int j=0;j<=s1+1;++j) {
                dp[i][j]=dp[i+1][j];
                for(int k=1;k<=j;++k) {
                    dp[i][j]=(dp[i][j]+dp[i+1][j-k]*C[s1-(j-k)][k-1]%MOD*f[k-1]%MOD*(k+1)%MOD)%MOD;
                }
            }
        } else {
            for(int j=0;j<=s1;++j) if(j>s0) {
                dp[i][j]=dp[i+1][j]*(j-s0)%MOD;
            }
        }
        s1+=a[i],s0+=1^a[i];
    }
    for(int i=1;i<=n;++i) dp[1][n]=dp[1][n]*i2%MOD;
    printf("%lld\n",dp[1][n]);
    return 0;
}

G. Constellation 3

Problem Link

题目大意

给定一个 \(n\times n\) 的矩形棋盘,第 \(i\) 列的 \((i,1)\sim (i,a_i)\) 从棋盘上被删掉。

在现有的棋盘上有 \(m\) 个点,每个星星有权值,你可以保留若干个点使得棋盘上不存在任何一个完整的矩形同时包含至少两个点,最小化删除点的权值和。

数据范围:\(n,m\le 2\times 10^5\)

思路分析

考虑建立大根笛卡尔树,对于笛卡尔树上的一个点 \(i\)\([l_i,r_i]\) 中高度 \(>a_i\) 的点不能超过 \(2\) 个。

因此有一个简单 dp,\(dp_{i,j}\) 表示 \(i\) 子树内保留若干个点,其中高度最大值为 \(j\)

那么转移的时候从左右儿子转移,设 \(x=\max_{j\le a_i} \{dp_{ls,j}\},y=\max_{j\le a_i}\{dp_{rs,j}\}\)

那么对于 \(j>a_i\)\(j\)\(dp_{i,j}=\max\{dp_{ls,j}+y,dp_{rs,j}+x\}\)

对于 \(j\le a_i\)\(j\),我们发现这些状态全部等价,我们只要记录其中的最大值,显然这个值是 \(x+y\)

考虑整体 dp,用线段树维护,支持区间加,单点赋值,查询区间最大值,线段树合并。

时间复杂度 \(\mathcal O((n+m)\log n)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
struct SegmentTree {
    static const int MAXS=8e6+5;
    int ls[MAXS],rs[MAXS],tot;
    ll mx[MAXS],tag[MAXS];
    inline void psu(int p) { mx[p]=max(mx[ls[p]],mx[rs[p]]); }
    inline void adt(int p,ll v) { if(p) mx[p]+=v,tag[p]+=v; }
    inline void psd(int p) { adt(ls[p],tag[p]),adt(rs[p],tag[p]),tag[p]=0; }
    inline void ins(int u,ll v,int l,int r,int &p) {
        if(!p) p=++tot;
        if(l==r) return mx[p]=max(mx[p],v),void();
        int mid=(l+r)>>1; psd(p);
        if(u<=mid) ins(u,v,l,mid,ls[p]);
        else ins(u,v,mid+1,r,rs[p]);
        psu(p);
    }
    inline void add(int ul,int ur,ll v,int l,int r,int p) {
        if(ul<=l&&r<=ur) return adt(p,v);
        int mid=(l+r)>>1; psd(p);
        if(ul<=mid) add(ul,ur,v,l,mid,ls[p]);
        if(mid<ur) add(ul,ur,v,mid+1,r,rs[p]);
        psu(p);
    }
    inline ll qry(int ul,int ur,int l,int r,int p) {
        if(ul<=l&&r<=ur) return mx[p];
        int mid=(l+r)>>1; psd(p);
        if(ur<=mid) return qry(ul,ur,l,mid,ls[p]);
        if(mid<ul) return qry(ul,ur,mid+1,r,rs[p]);
        return max(qry(ul,ur,l,mid,ls[p]),qry(ul,ur,mid+1,r,rs[p]));
    }
    inline void merge(int l,int r,int q,int &p) {
        if(!q||!p) return p|=q,void();
        if(l==r) return mx[p]=max(mx[p],mx[q]),void();
        int mid=(l+r)>>1; psd(p),psd(q);
        merge(l,mid,ls[q],ls[p]),merge(mid+1,r,rs[q],rs[p]);
        psu(p);
    }
}    T;
int n,m,a[MAXN],st[MAXN][20],rt[MAXN];
inline int cmp(int x,int y) { return a[x]>a[y]?x:y; }
struct Node { int x,y,w; };
vector <Node> pi[MAXN];
inline int bit(int x) { return 1<<x; }
inline int qry(int l,int r) {
    int k=__lg(r-l+1);
    return cmp(st[l][k],st[r-bit(k)+1][k]);
}
inline int solve(int l,int r) {
    int p=qry(l,r);
    for(auto q:pi[p]) T.ins(q.y,q.w,1,n,rt[p]);
    auto merge=[&](int u) {
        ll x=T.qry(1,a[p],1,n,rt[p]),y=T.qry(1,a[p],1,n,rt[u]);
        if(a[p]<n) T.add(a[p]+1,n,y,1,n,rt[p]),T.add(a[p]+1,n,x,1,n,rt[u]);
        T.merge(1,n,rt[u],rt[p]),T.ins(a[p],x+y,1,n,rt[p]);
    };
    if(l<p) merge(solve(l,p-1));
    if(p<r) merge(solve(p+1,r));
    return p;
}
signed main() {
    scanf("%d",&n);
    for(int i=1;i<=n;++i) scanf("%d",&a[i]),st[i][0]=i;
    for(int k=1;k<=20;++k) for(int i=1;i+bit(k-1)<=n;++i) {
        st[i][k]=cmp(st[i][k-1],st[i+bit(k-1)][k-1]);
    }
    scanf("%d",&m);
    ll sum=0;
    for(int i=1,x,y,w;i<=m;++i) scanf("%d%d%d",&x,&y,&w),pi[x].push_back({x,y,w}),sum+=w;
    printf("%lld\n",sum-T.mx[rt[solve(1,n)]]);
    return 0;
}

H. Harvest

Problem Link

题目大意

在一个长度为 \(L\) 的圆环上有 \(m\) 棵树和 \(n\) 个人,每个人都按顺时针方向以 \(1\) 的速度移动。

每当一个人经过一棵树时,如果此时距离这棵树上一次被采摘过去了至少 \(C\) 时刻,那么这个动点就会采摘一个果子。

\(q\) 次询问,\(t_i\) 秒后 \(v_i\) 会采摘几个果子。

数据范围:\(n,m,q\le 2\times 10^5\)

思路分析

注意到每个人速度相同,因此当一个人 \(i\) 采摘了某棵树,那么下一个来采摘这棵树的人是确定的,不妨称为 \(f_i\)

我们可以求出 \(i\) 采摘后过了多久 \(f_i\) 会来采摘,记为 \(w_i\),然后连边 \(i\to f_i\),边权为 \(w_i\),这显然会构成一棵基环内向树。

那么一棵树对答案的贡献就是从某个节点沿着树边开始不断向上走,边权就表示经过这条边要花费的时间,对经过的每个节点产生 \(+1\) 的贡献。

先预处理出每棵树 \(j\) 第一次被哪个人采摘,记为 \(v_j\),以及其花费的时间 \(t_j\)

先考虑 \(v_i\) 不在环上的情况,那么 \(j\to i\) 的贡献就是 \([t_j+dep(v_j)-dep(v_i)\le t_i]\),且 \(v_j\) 要在 \(v_i\) 子树里,其中 \(dep(u)\) 表示 \(u\) 在基环树上距离环的距离。

容易发现我们要求的就是 \(t_j+dep(v_j)\le t_i+dep(v_i)\),容易发现这是一个二维偏序,不需要显式离线下来解决,可以直接用 BIT 在 dfs 的时候维护:

每次我们搜到 \(v_i\) 的时候先求有在这之前 BIT 中有多少 \(\le t_i+dep(v_i)\) 的元素,然后搜索子树把子树里所有 \(t_j+dep(v_j)\) 加入 BIT,然后再在结束 dfs 的时候统计 BIT 中有多少 \(\le t_i+dep(v_i)\) 的元素,两者相减即是答案。

然后考虑 \(v_i\) 在环上的情况,此时对于每个棵树,我们求出其第一次跳到环上节点的时间 \(t_j\) 和对应节点标号 \(v_j\),然后删掉非环部分。

不妨设环为 \(c_1\gets c_2\gets c_3\gets \dots \gets c_k\gets c_1\),记 \(dis(u)\) 表示 \(u\) 距离 \(c_1\) 的距离,\(id_u\) 表示 \(u\) 在环上的标号,环长为 \(len\)

那么考虑 \(j\to i\) 的贡献:

  • \(id_j\ge id_i\),那么贡献为 \(\left\lfloor\dfrac{t_i-(t_j+dis(v_j)-dis(v_i))}{len}\right\rfloor+1\)
  • \(id_j<id_i\),那么贡献为 \(\left\lfloor\dfrac{t_i-(t_j+len-(dis(v_i)-dis(v_j)))}{len}\right\rfloor+1\),实际上等于 \(\left\lfloor\dfrac{t_i-(t_j+dis(v_j)-dis(v_i))}{len}\right\rfloor\)

当然这两个贡献要和 \(0\)\(\max\)

考虑通过比较两个项的余数来拆掉取整符号:设 \(q_i=\left\lfloor\dfrac{t_i+dis(v_i)}{len}\right\rfloor,r_i=t_i+dis(v_i)\bmod{len}\)

先考虑把 \(id_j\ge id_i\) 的情况,\(j\to i\) 的贡献就是 \(q_i-q_j-[r_j>r_i]+1=q_i-q_j+[r_j\le r_i]\),仅在 \(q_i\ge q_j\) 的时候统计贡献,容易保证贡献非负。

我们把 \(id_j<id_i\) 的类似先处理,当且仅当 \(t_i+dis(v_i)\ge t_j+dis(v_j)\) 时需要 \(-1\) 贡献,这又是一个二维偏序,离线下来 BIT 统计。

然后考虑 \(j\to i\) 的整体贡献,要求 \([q_i\ge q_j]\times (q_i-q_j)+[q_i\ge q_j]\times [r_j\le r_i]\),第一个很好维护,第二个又是一个二维偏序,离线下来双指针配合 BIT 统计即可。

对于每个基环树分别统计。

时间复杂度 \(\mathcal O((m+q)\log m)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
struct FenwickTree {
    ll n,tr[MAXN],s;
    vector <ll> id;
    inline void init() {
        sort(id.begin(),id.end());
        id.erase(unique(id.begin(),id.end()),id.end());
        n=id.size(),fill(tr+1,tr+n+1,0);
    }
    inline void add(ll x,ll v) {
        x=upper_bound(id.begin(),id.end(),x)-id.begin();
        for(;x<=n;x+=x&-x) tr[x]+=v;
    }
    inline ll qry(ll x) {
        x=upper_bound(id.begin(),id.end(),x)-id.begin();
        for(s=0;x;x&=x-1) s+=tr[x];
        return s;
    }
}    TR;
int n,m,q,fa[MAXN];
ll L,C,a[MAXN],b[MAXN],wf[MAXN];
struct Edge { int v; ll w; };
vector <Edge> G[MAXN];
vector <ll> vals[MAXN];

struct Query { ll t; int id;};
vector <Query> qry[MAXN];
ll ans[MAXN];

bool vis[MAXN],inc[MAXN];
ll dep[MAXN];
struct info {
    ll q,r; int id;
    inline friend bool operator <(const info &u,const info &v) {
        return u.q<v.q;
    }
};
inline void solve(int rt) {
    while(!vis[rt]) vis[rt]=true,rt=fa[rt];
    vector <int> cyc;
    while(!inc[rt]) inc[rt]=true,rt=fa[rt],cyc.push_back(rt);
    reverse(cyc.begin(),cyc.end());
    int k=cyc.size();
    cyc.push_back(cyc[0]);
    for(int i=0;i<k;++i) {
        function<void(int)>dfs0=[&](int u) {
            vis[u]=true;
            if(!inc[u]) {
                for(auto o:vals[u]) TR.id.push_back(o+dep[u]);
            }
            for(auto e:G[u]) if(!inc[e.v]) {
                dep[e.v]=dep[u]+e.w,dfs0(e.v);
            }
        };
        TR.id.clear(),dfs0(cyc[i]),TR.init();
        function<void(int)>dfs1=[&](int u) {
            vis[u]=true;
            if(!inc[u]) {
                for(auto o:qry[u]) ans[o.id]-=TR.qry(o.t+dep[u]);
                for(auto o:vals[u]) TR.add(o+dep[u],1),vals[cyc[i]].push_back(o+dep[u]);
            }
            for(auto e:G[u]) if(!inc[e.v]) dfs1(e.v);
            if(!inc[u]) {
                for(auto o:qry[u]) ans[o.id]+=TR.qry(o.t+dep[u]);
            }
        };
        dfs1(cyc[i]);
    }
    dep[0]=0;
    for(int i=1;i<=k;++i) dep[i]=dep[i-1]+wf[cyc[i]];
    ll len=dep[k];
    function<void(void)> calc1=[&]() {
        vector <info> pt,qy;
        TR.id.clear();
        for(int i=0;i<k;++i) {
            for(auto o:vals[cyc[i]]) {
                pt.push_back({(o+dep[i])/len,(o+dep[i])%len,0});
                TR.id.push_back((o+dep[i])%len);
            }
            for(auto o:qry[cyc[i]]) {
                qy.push_back({(o.t+dep[i])/len,(o.t+dep[i])%len,o.id});
            }
        }
        TR.init();
        sort(pt.begin(),pt.end());
        sort(qy.begin(),qy.end());
        auto it=pt.begin();
        ll cntq=0,sumq=0;
        for(auto o:qy) {
            while(it!=pt.end()&&it->q<=o.q) {
                TR.add(it->r,1),++cntq,sumq+=it->q,++it;
            }
            ans[o.id]+=cntq*o.q-sumq+TR.qry(o.r);
        }
    };
    calc1();
    function<void(void)> calc2=[&]() {
        TR.id.clear();
        for(int i=0;i<k;++i) {
            for(auto o:vals[cyc[i]]) TR.id.push_back(o+dep[i]);
        }
        TR.init();
        for(int i=0;i<k;++i) {
            for(auto o:qry[cyc[i]]) ans[o.id]-=TR.qry(o.t+dep[i]);
            for(auto o:vals[cyc[i]]) TR.add(o+dep[i],1);
        }
    };
    calc2();
}
signed main() {
    ios::sync_with_stdio(false);
    cin>>n>>m>>L>>C;
    for(int i=1;i<=n;++i) cin>>a[i];
    for(int i=1;i<=m;++i) cin>>b[i];
    for(int i=1;i<=n;++i) {
        fa[i]=upper_bound(a+1,a+n+1,(a[i]-C%L+L)%L)-a-1;
        if(!fa[i]) fa[i]=n;
        wf[i]=C/L*L+(a[i]-a[fa[i]]+L)%L;
        if(wf[i]<C) wf[i]+=L;
        G[fa[i]].push_back({i,wf[i]});
    }
    for(int i=1;i<=m;++i) {
        int x=upper_bound(a+1,a+n+1,b[i])-a-1;
        if(!x) x=n;
        vals[x].push_back((b[i]+L-a[x])%L);
    }
    cin>>q;
    for(int i=1;i<=q;++i) {
        int v; ll t;
        cin>>v>>t;
        qry[v].push_back({t,i});
    }
    for(int i=1;i<=n;++i) if(!vis[i]) solve(i);
    for(int i=1;i<=q;++i) cout<<ans[i]<<"\n";
    return 0;
}

I. Stray Cat

Problem Link

题目大意

通信题:

  • Anthony.cpp:给定一张 \(n\) 个点 \(m\) 条边的无向图,给每条边一个 \([0,A)\) 之间的标记。
  • Catherine.cpp:每次可以知道当前节点所有出边的标记(不包括来时的边),选定一个标记,随机走一条这个标记的边,或者原路返回。

要求对于任意一个起点 \(u\)Catherine.cpp 总能在 \(dis(0,u)+b\) 次移动内到达 \(0\)

数据范围:\(n,m\le 20000\)\(a=3,b=0\)\(a=2,b=6,m=n-1\)

思路分析

先考虑 \(a=3,b=0\) 的情况:

假如原图是一棵树,那么很好解决,对于每层的边,我们依次染 \(0,1,2\),那么 Catherine.cpp 每次就会得到 \((0,1),(1,2),(2,0)\),很好判断哪条向上哪条向下。

假如原图不是树,那么考虑建出 BFS 树,对于相邻两层之间的边按 \(0,1,2\) 顺次染,同层的边和下行边染同种颜色,这样就能保证每次都走上行边。

然后考虑 \(a=2,b=6,m=n-1\) 的情况:

考虑朴素做法,对于每层的边交替染 \(0,1\),我们把儿子到父亲的边称为上行边,父亲到儿子的边称为下行边。

容易发现起点不在链上的时候,一定能分出第一步的上下,而后面的上下也随之确定了:

  • 假如只有一个标记出现了 \(1\) 次,那么显然这条边就是上行边。
  • 假如两个标记都只出现了 \(1\) 次,那么和上一次移动不同的标记是上行边。

因此我们只要保证,每个点上行边和下行边颜色不同,那么我们一旦走出了一步正确的移动,那么就可以直接找到一条最短路。

因此我们只要解决第一步在链上的情况。

考虑把链上的边染成一个特殊的串,使得我们往一个方向走若干步后就能发现我们的运动方向到底是正还是反。

注意到 \(b=6\),因此我们至多可以向某个方向走 \(3\) 步,算上两端的一共是五条边,因为我们只要要求任意位置开始正着读五位始终与反着读五位不相同,可以构造出 \((001101)^\infty\) 满足条件。

如果链顶是 \(0\) 就从第一位开始,否则从第三位开始不断填链,如果是非链节点就 \(0,1\) 交替填。

注意特判没走够三步就到链外的情况。

时间复杂度 \(\mathcal O(n+m)\)

代码呈现

Anthony.cpp

#include<bits/stdc++.h>
#include "Anthony.h"
using namespace std;
namespace {
const int MAXN=2e4+5;
namespace Task0 {
int dep[MAXN];
vector <int> G[MAXN];
inline vector<int> main(int n,int m,vector<int>&u,vector<int>&v) {
    for(int i=0;i<m;++i) G[u[i]].push_back(v[i]),G[v[i]].push_back(u[i]);
    queue <int> Q; Q.push(0),dep[0]=1;
    while(Q.size()) {
        int x=Q.front(); Q.pop();
        for(int y:G[x]) if(!dep[y]) dep[y]=dep[x]+1,Q.push(y);
    }
    vector <int> w(m);
    for(int i=0;i<m;++i) w[i]=min(dep[u[i]],dep[v[i]])%3;
    return w;
}
}
namespace Task1 {
vector <int> w;
struct Edge { int v,id; };
vector <Edge> G[MAXN];
const int arr[]={0,0,1,1,0,1};
inline void dfs(int u,int fa,int len,int col) {
    if(fa==-1) {
        for(auto e:G[u]) w[e.id]=0,dfs(e.v,u,G[u].size()==1,1);
    } else if(G[u].size()==2) {
        if(len==0&&col==1) len=2;
        for(auto e:G[u]) if(e.v^fa) w[e.id]=arr[len%6],dfs(e.v,u,len+1,w[e.id]^1);
    } else {
        for(auto e:G[u]) if(e.v^fa) w[e.id]=col,dfs(e.v,u,0,col^1);
    }
}
inline vector<int> main(int n,int m,vector<int>&u,vector<int>&v) {
    for(int i=0;i<m;++i) G[u[i]].push_back({v[i],i}),G[v[i]].push_back({u[i],i});
    w.resize(m),dfs(0,-1,0,0);
    return w;
}
}
}
vector<int> Mark(int N,int M,int A,int B,vector<int> U,vector<int> V) {
    if(A>=3) return Task0::main(N,M,U,V);
    else return Task1::main(N,M,U,V);
}

Catherine.cpp

#include<bits/stdc++.h>
#include "Catherine.h"
using namespace std;
namespace {
bool task;
namespace Task0 {
inline int Move(vector<int>&g) {
    int cnt=0,s=0;
    for(int i:{0,1,2}) if(g[i]) ++cnt,s+=i;
    assert(cnt);
    if(cnt==1) return s;
    else return (4-s)%3; //f(0,1)=0, f(1,2)=1, f(2,0)=2
}
}
namespace Task1 {
const vector<int> mod[6]={{0,0,1,1,0},{0,1,1,0,1},{1,1,0,1,0},
                          {1,0,1,0,0},{0,1,0,0,1},{1,0,0,1,1}};
int lst=-1,chk=0;
vector <int> pat;
inline int solve(vector<int>&g) {
    if(lst==-1) { //first move
        if(g[0]+g[1]==2) { //chain
            if(g[0]==2) return pat={0,0},0;
            if(g[1]==2) return pat={1,1},1;
            return pat={0,1},1;
        } else { //tree
            chk=1;
            if(!g[0]||!g[1]) return g[0]?0:1;
            return (g[0]==1)?0:1;
        }
    }
    if(!g[0]&&!g[1]) return chk=1,-1; //leaf
    if(!chk) {
        if(g[0]+g[1]>1) { //tree
            ++g[lst],chk=1;
            if(g[0]==1&&lst!=0) return 0;
            if(g[1]==1&&lst!=1) return 1;
            return -1;
        }
        pat.push_back(g[0]?0:1); //chain
        if(pat.size()==5) {
            chk=1;
            for(int x:{0,1,2,3,4,5}) if(pat==mod[x]) return -1; //reverse
            return pat.back();
        } else return pat.back();
    } else {
        if(!g[0]||!g[1]) return g[0]?0:1; //chain
        if(g[0]==1&&g[1]==1) return lst^1; //3-deg
        return g[0]==1?0:1; //other
    }
}
inline int Move(vector<int>&g) { return lst=solve(g); }
}
}
void Init(int A,int B) { task=(A==2); }
int Move(vector<int> y) { return task?Task1::Move(y):Task0::Move(y); }

J. Capital City

Problem Link

题目大意

\(n\) 个点的树,每个点有一个 \([1,m]\) 之间的颜色,求一个最小的颜色集合 \(S\),使得颜色在 \(S\) 中的点两两连通。

数据范围:\(n\le 2\times 10^5\)

思路分析

考虑从一个点 \(u\) 开始确定一个极小联通块,先加入 \(c_u\),然后把所有该颜色的点到根的颜色加入,直到无法拓展,容易证明这样会得到包含 \(u\) 的最小的联通块。

然后我们要考虑不包含 \(u\) 的最小联通块,显然对 \(u\) 的每个子树问题都是独立的,我们对每个子树递归求解。

容易发现这是标准的点分治形式,每次取 \(u\) 为当前连通块的根即可。

时间复杂度 \(\mathcal O(n\log n)\)

代码呈现

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int MAXN=2e5+5;
vector <int> G[MAXN],C[MAXN];
int n,m,ans,siz[MAXN],cur[MAXN],col[MAXN],que[MAXN],ti[MAXN],fa[MAXN];
bool vis[MAXN],inq[MAXN];
inline void solve(int u) {
    function<void(int,int)> dfs3=[&](int x,int fz) {
        ti[x]=u,fa[x]=fz,inq[col[x]]=0;
        for(int y:G[x]) if(!vis[y]&&y!=fz) dfs3(y,x);
    };
    dfs3(u,0);
    int hd=1,tl=1,ok=1;
    inq[que[1]=col[u]]=true;
    while(hd<=tl) {
        int x=que[hd++];
        for(int v:C[x]) if(ti[v]!=u) return ;
        for(int v:C[x]) for(;v!=u&&!inq[col[fa[v]]];v=fa[v]) inq[que[++tl]=col[fa[v]]]=true;
    }
    if(ok) ans=min(ans,tl-1);
}
inline void dfs0(int u) {
    vis[u]=true,solve(u);
    for(int v:G[u]) if(!vis[v]) {
        function<void(int,int)> dfs1=[&](int x,int fz) {
            siz[x]=1;
            for(int y:G[x]) if(y!=fz&&!vis[y]) dfs1(y,x),siz[x]+=siz[y];
        };
        dfs1(v,u);
        int cet=0,alls=siz[v];
        function<void(int,int)> dfs2=[&](int x,int fz) {
            cur[x]=alls-siz[x];
            for(int y:G[x]) if(y!=fz&&!vis[y]) dfs2(y,x),cur[x]=max(cur[x],siz[y]);
            if(!cet||cur[x]<cur[cet]) cet=x;
        };
        dfs2(v,u),dfs0(cet);
    }
}
signed main() {
    scanf("%d%d",&n,&m),ans=m;
    for(int i=1,u,v;i<n;++i) scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
    for(int i=1;i<=n;++i) scanf("%d",&col[i]),C[col[i]].push_back(i);
    dfs0(1);
    printf("%d\n",ans);
    return 0;
}

K. Legendary Dango Maker

Problem Link

题目大意

本题为提交答案题。

你面前有一个 \(n\)\(m\) 列的网格,每格里面放着一个粉、白、绿三色之一的团子。你每次会横向、竖向或斜向选三个连续的团子并将他们按顺序串到一起。其中,按顺序指竖直方向的团子只能以上、中、下或下、中、上的顺序串,而不能以中、上、下或中、上、下的顺序串,其他顺序以此类推。这样,你就获得了一串团子。

当且仅当一串团子的颜色顺序是绿、白、粉或粉、白、绿时,我们把这串团子称为美丽串,请求出串取最多的美丽串的方法。

数据范围:\(n\le 500\)

思路分析

预处理出所有合法串,把不能同时被选的决策之间连边,原问题变成了一般图最大独立集问题。

考虑模拟退火,每次选一个不在独立集里的点,然后把其邻域中的点从独立集里删掉,然后再贪心加入被删除的点的邻域中可能加入的点。

设每次增广后带来的收益是 \(\delta\),就以 \(e^{\delta/T}\) 的概率接受,否则退回增广前的状态。

可以用 bitset 维护集合减小常数。

把初温调小一点,降温系数调大一点,简单挂一会就跑出来了。

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=505,MAXM=130011;
char mp[MAXN][MAXN];
vector <int> G[MAXM],adj[MAXN][MAXN];
int n,m,k=0,targ,xi[MAXM],yi[MAXM];
char di[MAXM];
inline void chk(int x,int y,int u,int v,char c) {
    function<bool(int,int)> valid=[&](int i,int j) {
        return 1<=i&&i<=n&&1<=j&&j<=m;
    };
    if(!valid(x+u,y+v)||!valid(x-u,y-v)) return ;
    if(mp[x][y]!='W'||mp[x+u][y+v]=='W'||mp[x-u][y-v]=='W') return ;
    if(mp[x+u][y+v]==mp[x-u][y-v]) return ;
    ++k,xi[k]=x,yi[k]=y,di[k]=c;
    for(int z:{-1,0,1}) {
        for(int q:adj[x+z*u][y+z*v]) G[q].push_back(k),G[k].push_back(q);
        adj[x+z*u][y+z*v].push_back(k);
    }
}
bitset <MAXM> res,now,tmp;
int ans,cur;
inline bool valid(int x) {
    if(now[x]) return false;
    for(int y:G[x]) if(now[y]) return false;
    return true;
}
mt19937 rnd(time(0));
signed main() {
    scanf("%d%d%d",&n,&m,&targ);
    for(int i=1;i<=n;++i) scanf("%s",mp[i]+1);
    for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) {
        chk(i,j,1,0,'|'),chk(i,j,0,1,'-'),chk(i,j,1,1,'\\'),chk(i,j,1,-1,'/');
    }
    for(int i=1;i<=k;++i) if(valid(i)) {
        ++ans,++cur,now[i]=res[i]=1;
    }
    for(long double T=1;ans<targ;T*=0.999999) {
        int id,bck=cur; tmp=now;
        do id=rnd()%k+1; while(now[id]);
        ++cur,now[id]=1;
        for(int x:G[id]) if(now[x]) {
            --cur,now[x]=0;
            for(int y:G[x]) if(valid(y)) ++cur,now[y]=1;
        }
        if(cur>ans) ans=cur,res=now;
        else if(cur>=bck||uniform_real_distribution<>(0,1)(rnd)<expl((cur-bck)/T)); 
        else cur=bck,swap(now,tmp);
    }
    for(int i=1;i<=k;++i) if(res[i]) mp[xi[i]][yi[i]]=di[i];
    for(int i=1;i<=n;++i) printf("%s\n",mp[i]+1);
    return 0;
}

L. Treatment Project

Problem Link

题目大意

给一个长度为 \(m\) 的序列,初始全是 \(1\),每个早上与 \(1\) 相邻的位置都会变成 \(1\),有 \(n\) 个操作,第 \(i\) 个会在第 \(t_i\) 天晚上把 \([l_i,r_i]\) 区间设成 \(0\),花费为 \(c_i\)

求把整个序列还原成全 \(0\) 的最小代价。

数据范围:\(n\le 10^5\)

思路分析

考虑把序列关于时间的图像画在二维平面上,那么对于一个操作,相当于把斜边为 \((t_i,l_i),(t_i,r_i)\) 的等腰直角三角形内部区域设成白色。

而如果两个等腰直角三角形相交,那么就会保留他们外侧的两条斜边生成一个更大的等腰直角三角形。

因此我们考虑 dp:\(dp_i\) 表示选若干个等腰直角三角形覆盖 \([1,r_i]\) 的最小花费,转移形如 \(dp_{i}+c_j\to dp_j\) 当且仅当 \(|t_i-t_j|\le r_i-l_j+1\)

容易发现这是一个最短路模型,可以直接主席树优化建图,当然也有更简单的做法:

注意到该图的权值附加在点上,在这种图上求 Dijkstra 时每个点只会加入堆中一次,因此直接按 \(t_i\) 排序建立线段树,每次取出所有能转移的 \(j\) 插进堆中即可。

时间复杂度 \(\mathcal O(n\log n)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5;
const ll inf=1e18;
int n,m;
struct Node { int l,r,t,c; } a[MAXN];
ll dp[MAXN],val[MAXN<<2][2];
bool del[MAXN];
priority_queue <array<ll,2>,vector<array<ll,2>>,greater<array<ll,2>>> Q;
inline void psu(int p) {
    val[p][0]=min(val[p<<1][0],val[p<<1|1][0]);
    val[p][1]=min(val[p<<1][1],val[p<<1|1][1]);
}
inline void build(int l=1,int r=n,int p=1) {
    if(l==r) {
        val[p][0]=del[l]?inf:a[l].l-a[l].t;
        val[p][1]=del[l]?inf:a[l].l+a[l].t;
        return ;
    }
    int mid=(l+r)>>1;
    build(l,mid,p<<1),build(mid+1,r,p<<1|1),psu(p);
}
inline void upd(int ul,int ur,int k,ll lim,ll w,int l=1,int r=n,int p=1) {
    if(val[p][k]>lim) return ;
    if(l==r) {
        Q.push({dp[l]=w+a[l].c,l});
        return val[p][0]=val[p][1]=inf,void();
    }
    int mid=(l+r)>>1;
    if(ul<=mid) upd(ul,ur,k,lim,w,l,mid,p<<1);
    if(mid<ur) upd(ul,ur,k,lim,w,mid+1,r,p<<1|1);
    psu(p);
}
signed main() {
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;++i) scanf("%d%d%d%d",&a[i].t,&a[i].l,&a[i].r,&a[i].c);
    sort(a+1,a+n+1,[&](auto x,auto y){ return x.t<y.t; });
    fill(dp+1,dp+n+1,inf);
    for(int i=1;i<=n;++i) if(a[i].l==1) Q.push({dp[i]=a[i].c,i}),del[i]=true;
    build();
    while(Q.size()) {
        int i=Q.top()[1]; Q.pop();
        upd(1,i,0,a[i].r-a[i].t+1,dp[i]);
        upd(i,n,1,a[i].r+a[i].t+1,dp[i]);
    }
    ll ans=inf;
    for(int i=1;i<=n;++i) if(a[i].r==m) ans=min(ans,dp[i]);
    printf("%lld\n",ans==inf?-1ll:ans);
    return 0;
}