JOISC2019 题解

发布时间 2023-12-09 14:12:18作者: DaiRuiChen007

Contest Link

\(\text{By DaiRuiChen007}\)

A. Examination

Problem Link

题目大意

\(n\) 个二元组 \((a,b)\)\(q\) 个询问 \((x,y,z)\),每个询问求满足 \(a\ge x\)\(b\ge y\)\(a+b\ge z\) 的二元组个数。

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

思路分析

直接 CDQ 求三维偏序即可,注意实现的常数。

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+1;
int ans[MAXN];
struct Node {
    int u,v,w,id;
}    a[MAXN];
class FenwickTree {
    private:
        int tree[MAXN];
        inline int lowbit(int x) { return x&-x; }
        vector <int> used;
    public:
        inline void Modify(int x,int v) {
            used.push_back(x);
            for(;x;x-=lowbit(x)) tree[x]+=v;
        }
        inline int Query(int x) {
            int ret=0;
            for(;x<MAXN;x+=lowbit(x)) ret+=tree[x];
            return ret;
        }
        inline void Clear() {
            for(int x:used) for(;x;x-=lowbit(x)) tree[x]=0;
            used.clear();
        }
}    TR;
inline void CDQ(int l,int r) {
    if(l==r) return ;
    int mid=(l+r)>>1;
    CDQ(l,mid),CDQ(mid+1,r);
    for(int lp=l,rp=mid;lp<=mid;++lp) {
        while(rp<r&&a[lp].v<=a[rp+1].v) {
            if(!a[++rp].id) TR.Modify(a[rp].w,1);
        }
        if(a[lp].id) ans[a[lp].id]+=TR.Query(a[lp].w);
    }
    TR.Clear();
    inplace_merge(a+l,a+mid+1,a+r+1,[&](Node x,Node y){ return x.v>y.v; });
}
signed main() {
    int n,q,N=0;
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;++i) {
        int s,t;
        scanf("%d%d",&s,&t);
        a[++N]={s,t,s+t,0};
    }
    for(int i=1;i<=q;++i) {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        a[++N]={x,y,z,i};
    }
    vector <int> X,Y,Z;
    for(int i=1;i<=N;++i) X.push_back(a[i].u),Y.push_back(a[i].v),Z.push_back(a[i].w);
    sort(X.begin(),X.end()),X.erase(unique(X.begin(),X.end()),X.end());
    sort(Y.begin(),Y.end()),Y.erase(unique(Y.begin(),Y.end()),Y.end());
    sort(Z.begin(),Z.end()),Z.erase(unique(Z.begin(),Z.end()),Z.end());
    for(int i=1;i<=N;++i) {
        a[i].u=lower_bound(X.begin(),X.end(),a[i].u)-X.begin()+1;
        a[i].v=lower_bound(Y.begin(),Y.end(),a[i].v)-Y.begin()+1;
        a[i].w=lower_bound(Z.begin(),Z.end(),a[i].w)-Z.begin()+1;
    }
    sort(a+1,a+N+1,[&](Node x,Node y){
        if(x.u!=y.u) return x.u<y.u;
        else if(x.v!=y.v) return x.v<y.v;
        else if(x.w!=y.w) return x.w<y.w;
        else return x.id>y.id;
    });
    CDQ(1,N);
    for(int i=1;i<=q;++i) printf("%d\n",ans[i]);
    return 0;
}

B. Meetings

Problem Link

题目大意

交互器有一棵 \(n\) 个点的树,每次你可以询问 \(u,v,w\),交互器会回答 \(cent(u,v,w)\) 即离他们距离和最小的点。

请在 \(10^5\) 次询问内还原整棵树。

数据范围:\(n\le 2000\),每个点度数不超过 \(18\)

思路分析

考虑不断扩展一个连通块,先从已知连通块为单点 \(rt\) 开始,随机一个点 \(u\),对于每个 \(i\):若 \(i=cent(rt,u,i)\) 那么 \(i\)\(rt\to u\) 的路径上,否则 \(cent(rt,u,i)\) 表示他在 \(rt\to u\) 路径上哪个点的子树里。

那么我们就知道删掉这条路径后形成的森林的每个连通块的根和点集,递归求解即可。

问题只剩对链排序,容易发现 \(rt\to u\) 的链中 \(x\)\(y\) 更靠近 \(rt\) 当且仅当 \(cent(rt,x,y)=x\),直接快排即可。

操作次数和时间复杂度不会证。

代码呈现

#include<bits/stdc++.h>
#include"meetings.h"
using namespace std;
mt19937 rnd(time(0));
void dfs(int rt,vector<int>&ver) {
    map <int,vector<int>> tr;
    vector <int> pat;
    int p=ver[rnd()%ver.size()];
    for(int u:ver) if(u!=p) {
        int v=Query(rt,p,u);
        if(v==u) pat.push_back(u);
        else tr[v].push_back(u);
    }
    sort(pat.begin(),pat.end(),[&](int x,int y){ return Query(rt,x,y)==x; });
    pat.insert(pat.begin(),rt),pat.push_back(p);
    for(int i=1;i<(int)pat.size();++i) Bridge(min(pat[i-1],pat[i]),max(pat[i-1],pat[i]));
    for(int u:pat) if(tr[u].size()) dfs(u,tr[u]);
}
void Solve(int N) {
    vector <int> ver(N-1);
    iota(ver.begin(),ver.end(),1);
    dfs(0,ver);
}

C. Naan

Problem Link

题目大意

有一个 \(m\) 厘米的线段,第 \([i-1,i)\) 厘米的部分被染上了颜色 \(i\)

另有 \(n\) 个人,每个人对每个颜色有一个喜好程度 \(v_{i,j}\),现在要将线段分割成连续的 \(n\) 段,每段给一个人。

每个人得到一段线段的开心值是线段上每个颜色对应的喜好程度乘上长度。最终的方案要满足第 \(i\) 个人的开心值大于等于 \(\dfrac 1n\sum_j v_{i,j}\)

给出方案或报告无解,输出分数,要求分母 \(\le 10^9\)

\(n,m\le 2000\)\(a_i\le 10^5\)

思路分析

对于每个人 \(i\),处理出 \(P_{i,1}\sim P_{i,n}\)\(P_{i,k}\) 表示取到 $\dfrac kn\sum_j v_{i,j} $ 的位置。

然后第一段就取 \(\min {P_{i,1}}\) 做右端点,然后分给对应的人 \(x\),第二段就在剩下的人中取 \(\min P_{i,2}\) 做右端点,然后分给对应的人 \(y\)

由于 \(P_{x,1}=\min P_{i,1}\),因此 \(P_{y,1}\ge P_{x,1}\),因此线段 \([P_{y,1},P_{y,2}]\subseteq[P_{x,1},P_{y,2}]\),第二段权值一定也合法,后面同理。

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

代码呈现

#include<bits/stdc++.h>
#define ll long long
#define LL __int128
using namespace std;
const int MAXN=2005;
struct frac {
    ll x,y;
    inline bool friend operator <(const frac &u,const frac &v) {
        return (LL)u.x*v.y<(LL)v.x*u.y;
    }
}    pos[MAXN][MAXN];
int n,m,bel[MAXN];
bool vis[MAXN];
ll a[MAXN][MAXN],tar[MAXN],tmp[MAXN];
signed main() {
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i) {
        for(int j=1;j<=m;++j) {
            scanf("%lld",&a[i][j]);
            tar[i]+=a[i][j];
        }
    }
    for(int i=1;i<=n;++i) {
        for(int j=1;j<=m;++j) tmp[j]=a[i][j]*n;
        for(int j=1,p=1;j<n;++j) {
            ll rem=tar[i];
            while(rem>tmp[p]) rem-=tmp[p++];
            tmp[p]-=rem;
            pos[i][j].y=n*a[i][p];
            pos[i][j].x=p*pos[i][j].y-tmp[p];
        }
    }
    for(int i=1;i<n;++i) {
        int p=0;
        for(int j=1;j<=n;++j) {
            if(!vis[j]&&(!p||pos[j][i]<pos[p][i])) p=j;
        }
        bel[i]=p,vis[p]=true;
        printf("%lld %lld\n",pos[p][i].x,pos[p][i].y);
    }
    for(int i=1;i<=n;++i) if(!vis[i]) bel[n]=i;
    for(int i=1;i<=n;++i) printf("%d ",bel[i]); puts("");
    return 0;
}

D. Two Anetennas

Problem Link

题目大意

给定 \(n\) 个点,每个点 \(i\) 会向标号在 \([i-b_i,i-a_i]\cup[i+a_i,i+b_i]\) 范围内的人连一条有向边。

\(q\) 次询问 \([l,r]\) 之中所有双向连边的点 \((i,j)\),最大的 \(|w_i-w_j|\) 是多少。

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

思路分析

显然 \(|w_i-w_j|=\max(w_i-w_j,w_j-w_i)\),因此每个点权值取法再跑一遍即可得到答案。

考虑扫描线,线段树维护每个 \(i\) 作为左端点的答案,把每个 \(i\)\(i+a_i\) 处加入,\(i+b_i+1\) 处删除。

对于每个 \(i\) 作为右端点,我们用 \(w_i\) 更新区间 \([i-b_i,i-a_i]\),线段树上维护 \(-w_j\) 的最大值和 \(w_i-w_j\) 的最大值,打标记的时候就很简单了。

注意删除 \(i\) 的时候只能把 \(-w_i\) 的最大值设成 \(-\infty\),不能改 \(w_i-w_j\) 的最大值,因为这些贡献依然存在。

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

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,inf=1e9;
int n,q;
struct SegmentTree {
    int tag[MAXN<<2],lmx[MAXN<<2],rmx[MAXN<<2];
    inline int L(int p) { return p<<1; }
    inline int R(int p) { return p<<1|1; }
    inline void psu(int p) {
        lmx[p]=max(lmx[L(p)],lmx[R(p)]);
        rmx[p]=max(rmx[L(p)],rmx[R(p)]);
    }
    inline void adt(int p,int k) {
        tag[p]=max(tag[p],k);
        rmx[p]=max(rmx[p],lmx[p]+k);
    }
    inline void psd(int p) {
        if(tag[p]!=-inf) adt(L(p),tag[p]),adt(R(p),tag[p]),tag[p]=-inf;
    }
    inline void build(int l=1,int r=n,int p=1) {
        tag[p]=lmx[p]=rmx[p]=-inf;
        if(l==r) return ;
        int mid=(l+r)>>1;
        build(l,mid,L(p)),build(mid+1,r,R(p));
    }
    inline void updl(int u,int k,int l=1,int r=n,int p=1) {
        if(l==r) return lmx[p]=k,void();
        int mid=(l+r)>>1; psd(p);
        if(u<=mid) updl(u,k,l,mid,L(p));
        else updl(u,k,mid+1,r,R(p));
        psu(p);
    }
    inline void updr(int ul,int ur,int k,int l=1,int r=n,int p=1) {
        if(ul<=l&&r<=ur) return adt(p,k);
        int mid=(l+r)>>1; psd(p);
        if(ul<=mid) updr(ul,ur,k,l,mid,L(p));
        if(mid<ur) updr(ul,ur,k,mid+1,r,R(p));
        psu(p);
    }
    inline int qry(int ul,int ur,int l=1,int r=n,int p=1) {
        if(ul<=l&&r<=ur) return rmx[p];
        int mid=(l+r)>>1; psd(p);
        if(ur<=mid) return qry(ul,ur,l,mid,L(p));
        if(mid<ul) return qry(ul,ur,mid+1,r,R(p));
        return max(qry(ul,ur,l,mid,L(p)),qry(ul,ur,mid+1,r,R(p)));
    }
}    TR;
struct Query { int l,r,id; };
vector <Query> Q[MAXN];
int h[MAXN],a[MAXN],b[MAXN],ans[MAXN];
vector <int> ins[MAXN<<1],ers[MAXN<<1];
inline void solve() {
    TR.build();
    for(int i=1;i<=n;++i) {
        for(int j:ins[i]) TR.updl(j,-h[j]);
        if(i>a[i]) TR.updr(max(1,i-b[i]),i-a[i],h[i]);
        for(auto x:Q[i]) ans[x.id]=max(ans[x.id],TR.qry(x.l,x.r));
        for(int j:ers[i]) TR.updl(j,-inf);
    }
}
signed main() {
    scanf("%d",&n);
    for(int i=1;i<=n;++i) {
        scanf("%d%d%d",&h[i],&a[i],&b[i]);
        ins[i+a[i]].push_back(i),ers[i+b[i]].push_back(i);
    }
    scanf("%d",&q);
    for(int i=1,l,r;i<=q;++i) scanf("%d%d",&l,&r),Q[r].push_back({l,r,i});
    fill(ans+1,ans+q+1,-1);
    solve();
    for(int i=1;i<=n;++i) h[i]*=-1;
    solve();
    for(int i=1;i<=q;++i) printf("%d\n",ans[i]);
    return 0;
}

E. Two Dishes

Problem Link

题目大意

给一个长度为 \(n\) 的序列 \(A\) 和一个长度为 \(m\) 的序列 \(B\),现在要将在保证相对顺序的前提下合并两个序列。

每个元素有三个参数 \(t_i,e_i,c_i\),表示任务 \(i\) 需要 \(t_i\) 秒完成,在第 \(e_i\) 秒前完成会获得 \(c_i\) 的分数。

求最大可能的分数和。

数据范围:\(n,m\le 10^6\)

思路分析

对于 \(A\) 中的元素,在他前面的 \(A\) 中元素是确定的,因此 \(e\) 的限制等价于限制 \(A_i\) 前面至多只能有 \(p_i\)\(B\) 才会有分数。

同理 \(B\) 里的元素限制为 \(B_i\) 前面至多只能有 \(q_i\)\(A\) 才会有分数。

考虑统一转化模型:

  • \(B_i\) 的限制等价于选 \(A_{q_i}\) 的时候如果已经选了 \(B_i\) 就会有 \(+w_i\) 的贡献。
  • \(A_i\) 的限制考虑反面考虑,先把 \(w_i\) 加入答案,选 \(A_{i}\) 的时候如果已经选了 \(B_{p_i}\) 就会有 \(-w_i\) 的贡献。

然后考虑维护 \(dp_{i,j}\) 表示插入 \(A_1\sim A_i,B_1\sim B_j\),对于所有 \(j\) 有数据结构维护,需要支持:若干次后缀加,然后取前缀 \(\max\)

std::setstd::map 维护差分数组,取前缀 \(\max\) 等价于不断删掉原有的差分项并把值相加,直到当前值非负。

显然每个差分项只会被删除一次,因此复杂度是对的。

注意我们实际上是每加一个数就取一次前缀 \(\max\),我们要先操作正数再操作负数才能保证正确性。

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

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5;
const ll inf=1e18;
ll a[MAXN],s[MAXN],p[MAXN],b[MAXN],t[MAXN],q[MAXN],ans=0;
vector <array<ll,2>> f[MAXN];
map <int,ll> dp;
inline void add(int id,ll val) {
    for(auto it=dp.lower_bound(id);val<0;it=dp.erase(it)) {
        id=it->first,val+=it->second;
    }
    dp[id]+=val;
}
signed main() {
    ios::sync_with_stdio(false);
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;++i) cin>>a[i]>>s[i]>>p[i],a[i]+=a[i-1];
    for(int i=1;i<=m;++i) cin>>b[i]>>t[i]>>q[i],b[i]+=b[i-1];
    for(int i=1;i<=n;++i) if(s[i]>=a[i]) {
        int j=upper_bound(b+1,b+m+1,s[i]-a[i])-b;
        ans+=p[i];
        if(j<=m) f[i].push_back({j,-p[i]});
    }
    for(int j=1;j<=m;++j) if(t[j]>=b[j]) {
        int i=upper_bound(a+1,a+n+1,t[j]-b[j])-a;
        if(i<=n) f[i].push_back({j,q[j]});
        else ans+=q[j];
    }
    dp[m+1]=inf;
    for(int i=1;i<=n;++i) {
        sort(f[i].begin(),f[i].end(),[&](auto x,auto y){ return x[1]>y[1]; });
        for(auto k:f[i]) add(k[0],k[1]);
    }
    for(auto x:dp) if(x.first<=m) ans+=x.second;
    cout<<ans<<"\n";
    return 0;
}

F. Two Transportations

Problem Link

题目大意

通信题。

给一张 \(n\) 个点 \(a+b\) 条带权无向边的图,其中 Azer.cpp 知道前 \(a\) 条边构成的图 \(G_A\)Baijan.cpp 知道后 \(b\) 条边构成的图 \(G_B\)

两个人都可以向对方发送信息(以二进制位形式),但相互发送信息的总量不能超过 \(58000\)\(\mathrm{bit}\)

最终要让 Azer.cpp 输出 \(0\) 到其他点的最短路。

数据范围:\(n\le 2000,w(e)\le 500\)

思路分析

考虑 Dijkstra,设已拓展点集为 \(Q\),那么下一步要求出 \(Q\) 之外离 \(0\) 最近的点 \(p\),容易发现这个点要么是 \(G_A\) 中离 \(0\) 最近的点 \(p_A\),要么是 \(G_B\) 中离 \(0\) 最近的点 \(p_B\)

那么做法就很简单了,先求出 \(p_A,p_B\) 以及对应的到 \(0\) 距离 \(d_A,d_B\)(显然与上一个 \(d\) 的差值不超过 \(500\),因此只需要传递差值即可),双方互相把自己求出的 \(d\) 发给对方,然后 \(d\) 较小的一方把 \(p\) 发给对方,这样就完成了一次拓展。

一次拓展的代价是 \(2\log w+\log n=29\),通信次数刚好是 \(58000\)

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

代码呈现

Azer.cpp

#include<bits/stdc++.h>
#include "Azer.h"
using namespace std;
const int inf=1e9;
int n,m,dA,dB,lstdis,vA,vB,typ,rdcnt,updcnt;
//typ=0:read dis,typ=1:read ver
vector <int> dis,vis;
struct Edge { int v,w; };
vector <vector<Edge>> G;
void upd(int u) {
    vis[u]=true,lstdis=dis[u];
    if(++updcnt==n) return ;
    for(auto e:G[u]) dis[e.v]=min(dis[e.v],dis[u]+e.w);
    vA=-1,dA=lstdis+511;
    for(int i=0;i<n;++i) if(!vis[i]&&dA>dis[i]) vA=i,dA=dis[i];
    dA-=lstdis,typ=0,rdcnt=9,dB=0;
    for(int k=8;~k;--k) SendA((dA>>k)&1);
}
void InitA(int N,int A,vector<int> U,vector<int> V,vector<int> C) {
    n=N,m=A,dis.resize(n,inf),vis.resize(n),G.resize(n);
    for(int i=0;i<m;++i) {
        G[U[i]].push_back({V[i],C[i]});
        G[V[i]].push_back({U[i],C[i]});
    }
    dis[0]=0,upd(0);
}
void ReceiveA(bool x) {
    if(typ) vB=vB*2+x;
    else dB=dB*2+x;
    if(!--rdcnt) {
        if(typ) dis[vB]=lstdis+dB,upd(vB);
        else {
            if(dA<=dB) {
                for(int k=10;~k;--k) SendA((vA>>k)&1);
                dis[vA]=lstdis+dA,upd(vA);
            } else typ=1,rdcnt=11,vB=0;
        }
    }
}
vector<int> Answer() { return dis; }

Baijan.cpp

#include<bits/stdc++.h>
#include "Baijan.h"
using namespace std;
const int inf=1e9;
int n,m,dA,dB,lstdis,vA,vB,typ,rdcnt,updcnt;
//typ=0:read dis,typ=1:read ver
vector <int> dis,vis;
struct Edge { int v,w; };
vector <vector<Edge>> G;
void upd(int u) {
    vis[u]=true,lstdis=dis[u];
    if(++updcnt==n) return ;
    for(auto e:G[u]) dis[e.v]=min(dis[e.v],dis[u]+e.w);
    vB=-1,dB=lstdis+511;
    for(int i=0;i<n;++i) if(!vis[i]&&dB>dis[i]) vB=i,dB=dis[i];
    dB-=lstdis,typ=0,rdcnt=9,dA=0;
    for(int k=8;~k;--k) SendB((dB>>k)&1);
}
void InitB(int N,int B,vector<int> U,vector<int> V,vector<int> C) {
    n=N,m=B,dis.resize(n,inf),vis.resize(n),G.resize(n);
    for(int i=0;i<m;++i) {
        G[U[i]].push_back({V[i],C[i]});
        G[V[i]].push_back({U[i],C[i]});
    }
    dis[0]=0,upd(0);
}
void ReceiveB(bool x) {
    if(typ) vA=vA*2+x;
    else dA=dA*2+x;
    if(!--rdcnt) {
        if(typ) dis[vA]=lstdis+dA,upd(vA);
        else {
            if(dB<dA) {
                for(int k=10;~k;--k) SendB((vB>>k)&1);
                dis[vB]=lstdis+dB,upd(vB);
            } else typ=1,rdcnt=11,vA=0;
        }
    }
}

G. Designated Cities

Problem Link

题目大意

给定一棵树,每条边有正向和反向,正向边权值为 \(c_i\),反向边权值为 \(d_i\)

定义一次操作为:选择一个点 \(u\),然后把以 \(u\) 为根的内向树上的有向边全部删掉。

\(q\) 次询问,每次给定 \(k\),求出进行 \(k\) 次操作后剩余的边权值和最小是多少。

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

思路分析

考虑贪心,可以证明在 \(k\ge 2\) 时,\(k+1\) 所选的答案一定是在 \(k\) 所选答案的基础上增加一个点。

\(k=1\) 的情形是简单换根,设以 \(x\) 为根的内向树权值和为 \(w(x)\)\(k=2\) 的情形就是最大化 \(\dfrac 12(w(x)+w(y)+\mathrm{dist}(x,y))\),其中 \(\mathrm{dist}(x,y)\) 的边权为 \(c_i+d_i\) ,显然求直径即可。

以直径一端为根,每次操作就是删掉一条链,这显然是经典的贪心结论,长链剖分后排序即可。

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

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
struct Edge { int v,x,y; }; //x:u->v,y:v<-u
vector <Edge> G[MAXN];
int n,q,fa[MAXN];
bool ind[MAXN];
ll sum=0,ans[MAXN],rt[MAXN],dis[MAXN];
inline void dfs0(int u,int fz) {
    for(auto e:G[u]) if(e.v^fz) rt[1]+=e.y,dfs0(e.v,u);
}
inline void dfs1(int u,int fz) {
    ans[1]=max(ans[1],rt[u]);
    for(auto e:G[u]) if(e.v^fz) rt[e.v]=rt[u]-e.y+e.x,dfs1(e.v,u);
}
inline void dfs2(int u,int fz,ll d,int &p) {
    fa[u]=fz,dis[u]=d;
    if(!p||dis[u]+rt[u]>dis[p]+rt[p]) p=u;
    for(auto e:G[u]) if(e.v^fz) dfs2(e.v,u,d+e.x+e.y,p);
}
vector <ll> len;
inline ll dfs3(int u,int fz) {
    ll mx=0;
    for(auto e:G[u]) if(e.v^fz) {
        ll tmp=e.x+dfs3(e.v,u);
        if(!mx) mx=tmp;
        else len.push_back(min(mx,tmp)),mx=max(mx,tmp);
    }
    return mx;
}
signed main() {
    scanf("%d",&n);
    for(int i=1,u,v,x,y;i<n;++i) {
        scanf("%d%d%d%d",&u,&v,&x,&y);
        sum+=x+y;
        G[u].push_back({v,x,y});
        G[v].push_back({u,y,x});
    }
    dfs0(1,0),dfs1(1,0);
    int S=0,T=0;
    dfs2(1,0,0,S);
    dfs2(S,0,0,T);
    ans[2]=(rt[S]+rt[T]+dis[T])>>1;
    for(int u=T;u;u=fa[u]) ind[u]=true;
    for(int u=T;u;u=fa[u]) {
        for(auto e:G[u]) if(e.v!=fa[u]&&!ind[e.v]) {
            len.push_back(dfs3(e.v,u)+e.x);
        }
    }
    sort(len.begin(),len.end(),greater<ll>());
    for(int i=0;i<(int)len.size();++i) ans[i+3]=ans[i+2]+len[i];
    for(int lst=len.size()+2,i=lst+1;i<=n;++i) ans[i]=ans[lst];
    scanf("%d",&q);
    for(int x;q--;) scanf("%d",&x),printf("%lld\n",sum-ans[x]);
    return 0;
}

H. Lamps

Problem Link

题目大意

给定两个 01 串 \(S,T\),每次操作可以对 \(S\) 进行区间覆盖或区间取反,求 \(S\to T\) 的最小操作次数。

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

思路分析

考虑观察解的形态:如果有一个取反操作在覆盖操作前面,显然可以通过调整把取反操作调整到覆盖操作后面。

而且显然取反操作区间不交,因此我们进行若干次覆盖得到 \(S'\),取反操作就是 \(S'\oplus T\) 的连续 \(1\) 个数。

显然简单 dp 即可,\(dp_{i,0/1/2}\) 表示处理前 \(S_1\sim S_i\),然后记录 \(S'_i\)\(0/1/S_i\) 即可 dp。

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

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
int n,dp[MAXN][3]; //0:set 0,1:set 1,2:do nothing
char a[MAXN],b[MAXN];
signed main() {
    scanf("%d%s%s",&n,a+1,b+1);
    memset(dp,0x3f,sizeof(dp));
    dp[1][0]=1+(b[1]!='0');
    dp[1][1]=1+(b[1]!='1');
    dp[1][2]=(b[1]!=a[1]);
    for(int i=2;i<=n;++i) for(int x:{0,1,2}) for(int y:{0,1,2}) {
        int c=dp[i-1][x];
        if(y<2&&x!=y) ++c;
        char vx=(x==2?a[i-1]:x+'0'),vy=(y==2?a[i]:y+'0');
        if(b[i]!=vy&&b[i-1]==vx) ++c;
        dp[i][y]=min(dp[i][y],c);
    }
    printf("%d\n",min({dp[n][0],dp[n][1],dp[n][2]}));
    return 0;
}

I. Bitaro, who Leaps through Time

Problem Link

题目大意

数轴上有 \(n\) 个城市,其中 \(i,i+1\) 之间有双向路径,想要通过必须在 \(x\in[l_i,r_i-1]\) 时刻出发,在 \(x+1\) 到达另一侧,你可以在原地让时刻 \(+1\) 或发动时光倒流使时刻 \(-1\)

\(q\) 次操作,修改 \(l_i,r_i\) 或询问 \(b\) 时刻在 \(a\) 至少要几次时光倒流才能在时刻 \(d\)\(c\)

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

思路分析

可以看成在网格图中向右下、下或上移动,考虑把右下操作变成右,即令 \([l_i,r_i-1]\gets [l_i-i,r_i-i-1]\)

然后这个问题就可以直接贪心,每次移动尽可能少,那么一个区间的限制有两种:

  • \([l,r]\) 表示在 \([l,r]\) 之内可以向另一侧移动。
  • \(x\to y\) 表示从 \(x\) 时刻进入 \(y\) 时刻输出,花费的代价为 \(w\)

分类讨论合并信息即可,线段树维护。

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

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5;
struct Info {
    int x,y; ll p; //p=-1: interval[x,y], p>=0: path x->y, cost = p
    inline friend Info operator +(const Info &u,const Info &v) {
        if(~u.p&&~v.p) return {u.x,v.y,u.p+max(0,u.y-v.x)+v.p};
        if(~u.p) return {u.x,max(min(u.y,v.y),v.x),u.p+max(0,u.y-v.y)};
        if(~v.p) return {min(max(u.x,v.x),u.y),v.y,v.p+max(0,u.x-v.x)};
        if(u.y<v.x) return {u.y,v.x,0};
        if(u.x>v.y) return {u.x,v.y,u.x-v.y};
        return {max(u.x,v.x),min(u.y,v.y),-1};
    }
};
int n,q;
struct SegmentTree {
    Info tr[MAXN<<2];
    inline void psu(int p) { tr[p]=tr[p<<1]+tr[p<<1|1]; }
    inline void upd(int u,Info I,int l=1,int r=n,int p=1) {
        if(l==r) return tr[p]=I,void();
        int mid=(l+r)>>1;
        if(u<=mid) upd(u,I,l,mid,p<<1);
        else upd(u,I,mid+1,r,p<<1|1);
        psu(p);
    }
    inline Info qry(int ul,int ur,int l=1,int r=n,int p=1) {
        if(ul<=l&&r<=ur) return tr[p];
        int mid=(l+r)>>1;
        if(ur<=mid) return qry(ul,ur,l,mid,p<<1);
        if(mid<ul) return qry(ul,ur,mid+1,r,p<<1|1);
        return qry(ul,ur,l,mid,p<<1)+qry(ul,ur,mid+1,r,p<<1|1);
    }
};
struct sol {
    SegmentTree T;
    inline void set(int i,int x,int y) { T.upd(i,{x-i,y-i-1,-1}); }
    inline ll qry(int u,int s,int v,int t) {
        return max(((Info){s-u,s-u,-1}+T.qry(u,v-1)+(Info){t-v,t-v,-1}).p,0ll);
    }
}    T1,T2;
int l[MAXN],r[MAXN];
signed main() {
    scanf("%d%d",&n,&q);
    for(int i=1;i<n;++i) {
        scanf("%d%d",&l[i],&r[i]);
        T1.set(i,l[i],r[i]),T2.set(n-i,l[i],r[i]);
    }
    for(int op,u,v,s,t;q--;) {
        scanf("%d",&op);
        if(op==1) {
            scanf("%d%d%d",&u,&s,&t);
            T1.set(u,s,t),T2.set(n-u,s,t);
        } else {
            scanf("%d%d%d%d",&u,&s,&v,&t);
            if(u==v) printf("%d\n",max(0,s-t));
            if(u<v) printf("%lld\n",T1.qry(u,s,v,t));
            if(u>v) printf("%lld\n",T2.qry(n-u+1,s,n-v+1,t));
        }
    }
    return 0;
}

J. Cake 3

Problem Link

题目大意

\(n\) 个元素中选 \(m\) 个并进行排列得到 \(s_1\sim s_m\),最大化 \(\sum_{i=1}^m v_i-|c_{i+1}-c_i|\)

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

思路分析

显然 \(c\) 的和最小为 \(2|c_{\max}-c_{\min}|\),按 \(c\) 小到大排序,区间 \([l,r]\) 的答案就是 \(S_k[l,r]-2\times(c_r-c_l)\),其中 \(S_k[l,r]\) 表示 \([l,r]\) 内的前 \(k\)\(v_i\) 值之和。

这是经典的决策单调性模型,分治优化,主席树维护权值即可。

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

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5,inf=1e9;
const ll infll=1e18;
struct SegmentTree {
    int ls[MAXN<<5],rs[MAXN<<5],cnt[MAXN<<5],tot;
    ll sum[MAXN<<5];
    inline void ins(int u,int l,int r,int q,int &p) {
        p=++tot,cnt[p]=cnt[q]+1,sum[p]=sum[q]+u;
        if(l==r) return ;
        int mid=(l+r)>>1;
        if(u<=mid) ins(u,l,mid,ls[q],ls[p]),rs[p]=rs[q];
        else ins(u,mid+1,r,rs[q],rs[p]),ls[p]=ls[q];
    }
    inline ll qry(int k,int l,int r,int q,int p) {
        if(l==r) return min(k,cnt[p])*l;
        int mid=(l+r)>>1,rc=cnt[rs[p]]-cnt[rs[q]];
        if(k<=rc) return qry(k,mid+1,r,rs[q],rs[p]);
        return sum[rs[p]]-sum[rs[q]]+qry(k-rc,l,mid,ls[q],ls[p]);
    }
}    T;
struct node { int h,v; } a[MAXN];
int n,m,rt[MAXN];
ll dp[MAXN];
inline void solve(int l,int r,int L,int R) {
    if(l>r) return ;
    int mid=(l+r)>>1,M=0;
    for(int i=L;i<=R&&i<=mid-m+1;++i) {
        ll val=T.qry(m,1,inf,rt[i-1],rt[mid])-2*(a[mid].h-a[i].h);
        if(val>dp[mid]) dp[mid]=val,M=i;
    }
    solve(l,mid-1,L,M),solve(mid+1,r,M,R);
}
signed main() {
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i) scanf("%d%d",&a[i].v,&a[i].h);
    sort(a+1,a+n+1,[&](auto x,auto y){ return x.h<y.h; });
    for(int i=1;i<=n;++i) T.ins(a[i].v,1,inf,rt[i-1],rt[i]);
    memset(dp,-0x3f,sizeof(dp));
    solve(m,n,1,n);
    ll ans=-infll;
    for(int i=m;i<=n;++i) ans=max(ans,dp[i]);
    printf("%lld\n",ans);
    return 0;
}

K. Mergers

Problem Link

题目大意

给定一棵 \(n\) 个点的树,每个点有颜色,定义一条边是分裂的当且仅当删掉这条边后形成了的两个连通块没有公共的颜色。

每次操作可以更改某类颜色的所有点变成另一个颜色,求让整棵树的边都不是分裂的的最小操作次数。

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

思路分析

先求出所有分裂的边,然后把不分裂的边缩起来,形成了一棵新的树,每次操作相当于选定两个点,把对应的路径上的分裂边变成不分裂的。

然后变成选若干路径覆盖整棵树,这是经典结论,设 \(k\) 为叶子数量,答案为 \(\left\lceil\dfrac k2\right\rceil\)

求分裂的边直接树上差分,把同色点之间的路径用树上差分打标记设成不分裂的。

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

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
vector <int> G[MAXN],C[MAXN];
int col[MAXN],fa[MAXN][20],dep[MAXN],cnt[MAXN],deg[MAXN];
inline void dfs0(int u,int fz) {
    fa[u][0]=fz,dep[u]=dep[fz]+1;
    for(int k=1;k<20;++k) fa[u][k]=fa[fa[u][k-1]][k-1];
    for(int v:G[u]) if(v^fz) dfs0(v,u);
}
inline void dfs1(int u,int fz) {
    for(int v:G[u]) if(v^fz) dfs1(v,u),cnt[u]+=cnt[v];
}
inline void dfs2(int u,int fz,int c) {
    for(int v:G[u]) if(v^fz) {
        if(!cnt[v]) ++deg[c],++deg[v],dfs2(v,u,v);
        else dfs2(v,u,c);
    }
}
inline int LCA(int u,int v) {
    if(dep[u]<dep[v]) swap(u,v);
    for(int k=19;~k;--k) if(dep[fa[u][k]]>=dep[v]) u=fa[u][k];
    if(u==v) return u;
    for(int k=19;~k;--k) if(fa[u][k]^fa[v][k]) u=fa[u][k],v=fa[v][k];
    return fa[u][0];
}
signed main() {
    int n,k;
    scanf("%d%d",&n,&k);
    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,0);
    for(int i=1;i<=k;++i) {
        for(int j=1;j<(int)C[i].size();++j) {
            ++cnt[C[i][0]],++cnt[C[i][j]],cnt[LCA(C[i][0],C[i][j])]-=2; 
        }
    }
    dfs1(1,0),dfs2(1,0,1);
    int ans=0;
    for(int i=1;i<=n;++i) if(!cnt[i]&&deg[i]==1) ++ans;
    printf("%d\n",(ans+1)/2);
    return 0;
}

L. Minerals

Problem Link

题目大意

交互器有 \(2n\) 个元素,每个元素有颜色,每种颜色恰好两个元素。

你有一个集合 \(S\),你可以进行 \(10^6\) 次操作,每次操作会改变某个元素 \(x\)\(S\) 中的状态,然后返回 \(S\) 中元素的颜色数。

请你分别求出每一对同色元素。

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

思路分析

考虑不断递归:每次维护两个集合 \(L,R\),其中每种颜色都在 \(L\) 中出现一次,\(R\) 中出现一次。

然后我们把 \(L\) 分成两个集合 \(L_X,L_Y\),然后令 \(S=L_X\),每次加入 \(R\) 中元素 \(r\)

如果答案变大就放进 \(R_Y\),否则就放进 \(R_X\),然后递归 \((L_X,R_X),(L_Y,R_Y)\)

考虑如何求初始的 \(L,R\),这是简单的,每次加入一个点,颜色数变多就放进 \(L\),否则放进 \(R\)

操作次数 \(4n+3n\log n\),期望得分 \(40\)

考虑优化,注意到我们不关心每次的 \(r\) 是从 \(S\) 中删除还是加入,只要答案数变化就放进 \(R_Y\),否则放进 \(R_X\)

操作次数 \(3n+2n\log n\),期望得分 \(40\)

然后考虑避免清空 \(L_X\),这也是可以的,我们只要 \(L_X\)\(L_Y\) 里的点在 \(S\) 的状态不同即可,我们递归时记录当前是否有 \(L\subseteq S\),如果有就交换 \(L_X,L_Y\) 即可。

操作次数 \(2n+\dfrac 32n\log n\),期望得分 \(80\)

接着考虑优化,注意到每次操作的代价是 \(|L_X|+|R|\),因此如果我们稍微调小一点 \(L_x\) 也许有优化效果。

考虑 dp 找出转移点,\(f_i\) 表示 \(|L|=|R|=i\) 时的答案,那么 \(f_i=\min_{j=1}^{i/2}\{f_j+f_{i-j}+i+j\}\)

打表观察到转移点具有单调性,可以二分加单调队列优化,但暴力从上一个决策点的位置转移已经足够通过此题。

操作次数 \(2n+f_n\)\(f_{43000}=956975\),期望得分 \(85\)

考虑接着优化,注意到我们求 \(R_X\)\(R_Y\) 时可以减掉一次询问,即最后一次询问直接根据两边大小判断,塞进没满的一边即可。

此时转移变成 \(f_i=\min_{j=1}^{i/2}\{f_j+f_{i-j}+i+j-1\}\),此时 \(f_{43000}=913976\)\(f_n+2n=999976\),刚好通过此题。

事实上,在 \(R_X/R_Y\) 中的一个填满后立刻把剩余元素填到另一边优化效果更明显。

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

代码呈现

#include<bits/stdc++.h>
#include"minerals.h"
using namespace std;
const int MAXN=43005;
int lst=0,cur,f[MAXN],p[MAXN];
inline bool qry(int x) {
    cur=Query(x);
    if(lst==cur) return false;
    return lst=cur,true;
}
inline void solve(vector<int>&L,vector<int>&R,bool rv) {
    int n=L.size();
    if(n==1) return Answer(L[0],R[0]);
    auto it=L.begin()+p[n];
    vector <int> LX(L.begin(),it),LY(it,L.end()),RX,RY;
    for(int i:LX) qry(i);
    if(rv) swap(LX,LY);
    for(int i=0;i<n-1;++i) (qry(R[i])?RY:RX).push_back(R[i]);
    (RX.size()<LX.size()?RX:RY).push_back(R[n-1]);
    solve(LX,RX,1),solve(LY,RY,0);
}
void Solve(int N) {
    memset(f,0x3f,sizeof(f)),p[1]=1,f[1]=0;
    for(int i=2;i<=N;++i) {
        for(int j=p[i-1];j<=i/2;++j) {
            int g=f[j]+f[i-j]+i+j-1;
            if(g<f[i]) p[i]=j,f[i]=g;
        }
    }
    vector <int> L,R;
    for(int i=1;i<=2*N;++i) (qry(i)?L:R).push_back(i);
    solve(L,R,1);
}