JOISC2019 簡要題解

第18回 日本情報オリンピック 春合宿 オンラインコンテスト (JOISC2019)

官網node

Day 1

試験 (Examination)

description

\(N\)個學生,每一個學生有兩科成績\(S_i,T_i\)。定義一個學生合格當且僅當他的第一科成績\(\ge A\),第二科成績\(\ge B\)且總成績\(\ge C\)。給出\(Q\)\((A_i,B_i,C_i)\),問每組限制要求下有多少學生合格。算法

\(N,Q\le10^5\)數組

solution

裸的三維數點?\(CDQ\)練習題?app

#include<cstdio>
#include<algorithm>
using namespace std;
int gi(){
    int x=0,w=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')w=0,ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}
const int N=2e5+5;
struct node{
    int x,y,z,id;
    bool operator < (const node &b)const
        {return z>b.z||z==b.z&&id<b.id;}
}p[N],tmp[N];
int n,q,o[N],len,bit[N],ans[N];
void modify(int x){
    while(x<=len)++bit[x],x+=x&-x;
}
int query(int x){
    int res=0;
    while(x)res+=bit[x],x^=x&-x;
    return res;
}
void clear(int x){
    while(x<=len)bit[x]=0,x+=x&-x;
}
void solve(int l,int r){
    if(l==r)return;int mid=l+r>>1;solve(l,mid);solve(mid+1,r);
    for(int i=l,j=l,k=mid+1;i<=r;++i)
        if(j<=mid&&(k>r||p[j].x>=p[k].x)){
            tmp[i]=p[j++];
            if(!tmp[i].id)modify(tmp[i].y);
        }else{
            tmp[i]=p[k++];
            if(tmp[i].id)ans[tmp[i].id]+=query(tmp[i].y);
        }
    for(int i=l;i<=mid;++i)if(!p[i].id)clear(p[i].y);
    for(int i=l;i<=r;++i)p[i]=tmp[i];
}
int main(){
    n=gi();q=gi();
    for(int i=1;i<=n;++i)p[i]=(node){gi(),gi(),0,0},p[i].z=p[i].x+p[i].y;
    for(int i=1;i<=q;++i)p[i+n]=(node){gi(),gi(),gi(),i};
    for(int i=1;i<=n+q;++i)o[++len]=p[i].y;
    sort(o+1,o+len+1);len=unique(o+1,o+len+1)-o-1;
    for(int i=1;i<=n+q;++i)p[i].y=len-(lower_bound(o+1,o+len+1,p[i].y)-o)+1;
    sort(p+1,p+n+q+1);solve(1,n+q);
    for(int i=1;i<=q;++i)printf("%d\n",ans[i]);return 0;
}

ナン (Naan)

description

有一條長度爲\(L\)的麪包被從左至右分紅了\(L\)段,每段長度都是\(1\)。每一段的味道都不一樣,從左至右第\(i\)段的味道是\(i\)。有\(N\)我的要來瓜分這塊麪包,他們打算在麪包上切\(N-1\)刀切成\(N\)段而後一人拿走一段。每一個人對每種味道都有一種喜好值,當第\(i\)我的拿了\(1\)長度的味道\(j\)的麪包時他會得到\(V_{i,j}\)的愉悅值。當第\(i\)我的在某種劃分方案中拿到的麪包片斷使他獲得的愉悅值很多於\(\frac{\sum_{j=1}^LV_{i,j}}{L}\)時他就會偷稅。求一種使全部人都偷稅的劃分方案,要求輸出\(N-1\)個切割點(以分數的形式)以及一個排列\(P\)表示拿麪包的順序。函數

\(N,L\le2000\)優化

solution

對每一個人預處理將麪包劃分紅\(N\)段使每段的愉悅值相等的切割點。而後在第\(i\)次切割時,找到剩下沒拿麪包的人的最小切割點便可。這樣貪心的正確性顯然。ui

#include<cstdio>
#include<algorithm>
using namespace std;
int gi(){
    int x=0,w=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')w=0,ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}
#define ll long long
#define pi pair<ll,ll>
#define mk make_pair
#define fi first
#define se second
const int N=2005;
pi p[N][N],ans1[N];int n,m,val[N],vis[N],ans2[N];
bool cmp(pi a,pi b){return (long double)a.fi/a.se<(long double)b.fi/b.se;}
int main(){
    n=gi();m=gi();
    for(int i=1;i<=n;++i){
        ll sum=0,now=0;
        for(int j=1;j<=m;++j)sum+=(val[j]=gi());
        for(int j=1,k=1;j<=n;++j){
            while(k<m&&(now+val[k])*n<sum*j)now+=val[k++];
            ll a=1ll*n*val[k]*(k-1)+sum*j-now*n,b=1ll*n*val[k],d=__gcd(a,b);
            p[i][j]=mk(a/d,b/d);
        }
    }
    for(int i=1;i<=n;++i){
        int x=0;p[x][i]=mk(1,0);
        for(int j=1;j<=n;++j)if(!vis[j])x=cmp(p[j][i],p[x][i])?j:x;
        ans1[i]=p[x][i];ans2[i]=x;vis[x]=1;
    }
    for(int i=1;i<n;++i)printf("%lld %lld\n",ans1[i].fi,ans1[i].se);
    for(int i=1;i<=n;++i)printf("%d ",ans2[i]);return puts(""),0;
}

ビーバーの會合 (Meetings)

description

交互題。spa

有一棵樹,保證每一個點的度數不超過\(18\)。每次能夠詢問三個點\((a,b,c)\),交互庫會返回一個點\(d\)使\(dis(a,d)+dis(b,d)+dis(c,d)\)最小(顯然這樣的\(d\)是惟一的)。你須要在不超過\(40000\)次詢問內還原出樹的形態。設計

\(n \le 2000\)code

solution

翻車現場?

原題保證樹隨機生成且不隨詢問而改變,次數限制是\(25000\)。作法是每次隨機兩個點\(a,b\),枚舉剩下的每一個點\(c\)並詢問\((a,b,c)\),若返回\(d=c\)則說明\(c\)\(a\)\(b\)的路徑上,不然說明\(c\)\(d\)的子樹中。這樣就能夠找出\(a\)\(b\)路徑上的全部點,經過一次std::sort能夠求出路徑上全部點的順序,即肯定這條路徑。而後對於不在這條路徑上的點,枚舉路徑上的每一個點的子樹,遞歸處理便可。

至於這題,好像直接粘代碼就過了?

複雜度不太會證,哪位哥哥教教我呀\(Q\omega Q\)

#include"meetings.h"
#include<algorithm>
#include<vector>
using namespace std;

unsigned int rng(){
    static unsigned int x=141905,y=141936,z=19260817;
    x^=x<<15;x^=x>>6;x^=x<<1;
    unsigned int w=x;x=y;y=z;z^=w^x;return z;
}
int n,p;
bool cmp(int i,int j){int k=Query(p,i,j);return k==i;}
void work(vector<int>vec,int x){
    vector<int>chain;vector<vector<int> >nxt(n);
    int y=vec[rng()%vec.size()];
    for(int z:vec){
        if(z==y)continue;
        int w=Query(x,y,z);
        if(w==z)chain.push_back(z);
        else nxt[w].push_back(z);
    }
    p=x;sort(chain.begin(),chain.end(),cmp);chain.push_back(y);
    for(int z:chain)Bridge(min(p,z),max(p,z)),p=z;
    if(nxt[x].size())work(nxt[x],x);
    for(int z:chain)if(nxt[z].size())work(nxt[z],z);
}
void Solve(int _n){
    n=_n;vector<int>tmp;
    for(int i=1;i<n;++i)tmp.push_back(i);
    work(tmp,0);
}

Day 2

ふたつのアンテナ (Two Antennas)

description

\(N\)座信號塔排成一排,第\(i\)座的位置爲\(i\),高度爲\(H_i\),只能與距離它\([A_i,B_i]\)的信號塔通訊。若兩座信號塔\(i,j\)能夠互相通訊,那麼它們就會產生\(|H_i-H_j|\)的代價。\(Q\)組詢問,每次給一個區間\([L_i,R_i]\),求區間內相互通訊的信號塔產生的最大代價。

\(N,Q\le2\times10^5\)

solution

先只考慮\(i<j,H_i>H_j\)的狀況,\(H_i<H_j\)的狀況只須要翻轉值域後再作一遍就好了。

對於一個\(i\),它能夠與\([i+A_i,i+B_i]\)內的信號塔通訊。咱們從左至右枚舉\(j\),並將信號塔\(i\)拆成兩個事件:在\([i+A_i]\)時刻信號塔\(i\)變得可用,在\([i+B_i+1]\)時刻信號塔\(i\)再也不可用。因而在枚舉到信號塔\(j\)時,它可以互相通訊的信號塔就是\([j-B_j,j-A_j]\)內全部可用的信號塔。咱們維護\(d_i\)表示每一個\(i\)做爲左邊信號塔時產生的最大代價,那麼每新加入一個\(j\)後就會將\([j-B_j,j-A_j]\)內的全部可用信號塔的\(d_i\)\(H_i-H_j\)\(\max\),詢問\([L,R]\)的答案就是當枚舉到\(j=R\)時的\(\max_{L\le i\le R}d_i\)

咱們對每一個信號塔定義一個\(c_i\),當其可用時\(c_i=H_i\),不然\(c_i=-\infty\)。那麼每一個信號塔的兩個事件是對\(c\)數組的單點修改,每加入一個\(j\)就是將一段區間內的\(d_i\)\(c_i-H_j\)\(\max\),詢問依然是求區間\(d_i\)的最大值。

以上三種操做都可以用線段樹實現。複雜度\(O(n\log n)\)

#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
int gi(){
    int x=0,w=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')w=0,ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}
#define pi pair<int,int>
#define mk make_pair
#define fi first
#define se second
const int N=2e5+5;
const int inf=1<<30;
int n,q,h[N],a[N],b[N],c[N<<2],d[N<<2],tag[N<<2],ans[N];
vector<pi>E[N],Q[N];
void build(int x,int l,int r){
    tag[x]=c[x]=d[x]=-inf;if(l==r)return;
    int mid=l+r>>1;build(x<<1,l,mid);build(x<<1|1,mid+1,r);
}
void up(int x){
    c[x]=max(c[x<<1],c[x<<1|1]);d[x]=max(d[x<<1],d[x<<1|1]);
}
void cover(int x,int v){
    tag[x]=max(tag[x],v);d[x]=max(d[x],c[x]+tag[x]);
}
void down(int x){
    if(tag[x]==-inf)return;
    cover(x<<1,tag[x]);cover(x<<1|1,tag[x]);tag[x]=-inf;
}
void modify(int x,int l,int r,int p,int v){
    if(l==r){tag[x]=-inf;c[x]=v;return;}
    down(x);int mid=l+r>>1;
    p<=mid?modify(x<<1,l,mid,p,v):modify(x<<1|1,mid+1,r,p,v);
    up(x);
}
void update(int x,int l,int r,int ql,int qr,int v){
    if(l>=ql&&r<=qr){cover(x,v);return;}
    down(x);int mid=l+r>>1;
    if(ql<=mid)update(x<<1,l,mid,ql,qr,v);
    if(qr>mid)update(x<<1|1,mid+1,r,ql,qr,v);
    up(x);
}
int query(int x,int l,int r,int ql,int qr){
    if(l>=ql&&r<=qr)return d[x];
    down(x);int mid=l+r>>1,res=-inf;
    if(ql<=mid)res=max(res,query(x<<1,l,mid,ql,qr));
    if(qr>mid)res=max(res,query(x<<1|1,mid+1,r,ql,qr));
    return res;
}
void work(){
    build(1,1,n);
    for(int i=1;i<=n;++i){
        for(pi p:E[i])modify(1,1,n,p.fi,p.se?h[p.fi]:-inf);
        if(i>a[i])update(1,1,n,max(i-b[i],1),i-a[i],-h[i]);
        for(pi p:Q[i])ans[p.fi]=max(ans[p.fi],query(1,1,n,p.se,i));
    }
}
int main(){
    n=gi();
    for(int i=1;i<=n;++i){
        h[i]=gi(),a[i]=gi(),b[i]=gi();
        if(i+a[i]<=n)E[i+a[i]].push_back(mk(i,1));
        if(i+b[i]+1<=n)E[i+b[i]+1].push_back(mk(i,0));
    }
    q=gi();
    for(int i=1,l;i<=q;++i)l=gi(),Q[gi()].push_back(mk(i,l)),ans[i]=-1;
    work();for(int i=1;i<=n;++i)h[i]=1000000000-h[i];work();
    for(int i=1;i<=q;++i)printf("%d\n",ans[i]);return 0;
}

ふたつの料理 (Two Dishes)

description

你要作兩道菜,第一道菜有\(n\)個步驟,第\(i\)個步驟耗時\(A_i\),若在\(S_i\)時刻內作完該步驟便可得到\(P_i\)的收益;第二道菜有\(m\)個步驟,第\(j\)步耗時\(B_j\),若在\(T_j\)時刻作完該步驟便可得到\(Q_j\)的收益。求最大收益。

\(n,m\le10^6\)

solution

對每一個\(i\in[1,n]\)求出\(y_i=\max\{j|\sum_{k=1}^iA_k+\sum_{k=1}^jB_k\le S_i\}\),同理對每一個\(j\in[1,m]\)也求出\(x_j=\max\{i|\sum_{k=1}^iA_i+\sum_{k=1}^j\le T_j\}\)

將作菜的過程轉化爲在格點圖上的行走過程,即要從\((0,0)\)走到\((n,m)\),每步能夠向上走或向右走一格。將上述求出的點\((i,y_i)\)與點\((x_j,j)\)放到格點圖上。能夠發現,當且僅當點\((i,y_i)\)在路徑的上方或者在路徑上時,能夠產生\(P_i\)的貢獻。相對的,當且僅當點\((x_j,j)\)在路徑的下方或者在路徑上時,能夠產生\(Q_j\)的貢獻。

兩種產生貢獻的方式貌似有些難以處理。不過考慮這樣一件事情:一個點\((x,y)\)不在路徑的上方或路徑上當且僅當點\((x+1,y-1)\)在路徑的下方或路徑上,所以咱們先將全部的\(P_i\)加入答案,再把不知足的點的貢獻減去便可。

因而如今模型轉化成了:在格點圖上找到一條路徑,最大化路徑下方以及路徑上的點的權值之和。不難寫出一個\(O(nm)\)\(dp\)式:

\[f_{i,j}=\max(f_{i,j-1},f_{i-1,j}+sum_{i,j})\]

其中\(sum_{i,j}\)表示\((i,j)\)正下方的點的權值之和,最終的答案是\(f_{n-1,m}+sum_{n,m}\)

不難發現這個\(dp\)的實質是對於每一個\(i\),先進行若干次後綴修改(加上某個數),再維護一遍前綴最大值。咱們能夠維護\(dp\)數組的前綴最大值的差分數組,這樣每次修改變成了單點加,維護前綴最大值的就是將差分數組中的全部負數消去(與其後方的正數抵消,若後方不存在正數則直接刪去)。用線段樹+std::set簡單維護一下就好了。

#include<cstdio>
#include<algorithm>
#include<set>
using namespace std;
#define ll long long
ll gi(){
    ll x=0,w=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')w=0,ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}
const int N=1e6+5;
struct node{
    int x,y;ll z;
    bool operator < (const node &b)const
        {return x<b.x;}
}a[N<<1];
int n,m,tot,pos[N<<2];
ll A[N],S[N],P[N],B[N],T[N],Q[N],val[N],ans;
set<int>Set;set<int>::iterator it;
void up(int x){pos[x]=val[pos[x<<1]]<val[pos[x<<1|1]]?pos[x<<1]:pos[x<<1|1];}
void build(int x,int l,int r){
    if(l==r){pos[x]=l;return;}int mid=l+r>>1;
    build(x<<1,l,mid);build(x<<1|1,mid+1,r);up(x);
}
void modify(int x,int l,int r,int p,ll v){
    if(l==r){
        val[l]+=v;
        if(val[l])Set.insert(l);else Set.erase(l);
        return;
    }
    int mid=l+r>>1;p<=mid?modify(x<<1,l,mid,p,v):modify(x<<1|1,mid+1,r,p,v);up(x);
}
int main(){
    n=gi();m=gi();
    for(int i=1;i<=n;++i)A[i]=A[i-1]+gi(),S[i]=gi(),P[i]=gi();
    for(int i=1;i<=m;++i)B[i]=B[i-1]+gi(),T[i]=gi(),Q[i]=gi();
    for(int i=1;i<=n;++i)
        if(A[i]<=S[i]){
            ans+=P[i];int p=upper_bound(B+1,B+m+1,S[i]-A[i])-B;
            if(p<=m)a[++tot]=(node){i-1,p,-P[i]};
        }
    for(int i=1;i<=m;++i)
        if(B[i]<=T[i]){
            int p=upper_bound(A+1,A+n+1,T[i]-B[i])-A-1;
            a[++tot]=(node){p,i,Q[i]};
        }
    a[++tot]=(node){n,1,0};
    sort(a+1,a+tot+1);build(1,1,m);
    for(int i=1,j=1;i<=tot;i=j=j+1){
        while(j<tot&&a[j+1].x==a[i].x)++j;
        if(a[i].x==n){
            for(int k=i;k<=j;++k)ans+=a[k].z;
            for(int x:Set)ans+=val[x];
            printf("%lld\n",ans);
            return 0;
        }
        for(int k=i;k<=j;++k)modify(1,1,m,a[k].y,a[k].z);
        while(val[pos[1]]<0){
            int p=pos[1];ll v=val[p];
            it=Set.find(p);++it;
            if(it!=Set.end())modify(1,1,m,*it,v);
            modify(1,1,m,p,-v);
        }
    }
}

ふたつの交通機関 (Two Transportations)

description

通訊題。

\(A\)和小\(B\)分別拿到了一張\(N\)個點\(M\)條邊的無向圖,兩張圖的點數相同而邊數可能不一樣,每條邊鏈接兩個點\(u_i,v_i\),邊長爲\(w_i\)。兩人之間總共能夠互發至多\(58000\)\(\mbox{bits}\),須要讓小\(A\)知道兩張圖上的邊合在一塊兒後從\(0\)出發到全部點的最短路。

通訊的具體實現方式是這樣的:你須要實現兩個函數ReceiveA(bool x)ReceiveB(bool x),有兩個std::queue用於存儲兩人之間互發的\(\mbox{bits}\),每次交互庫會選擇一個非空的std::queue並調用一次相對應的Receive函數,若二者均非空則會按某種方式選擇調用其一,若二者均爲空時視做已經求出答案。

\(N\le2000,M\le5\times10^5,0\le u_i,v_i<N,1\le w_i\le500\)

solution

\(N\le2000\)的圖上求最短路怎麼作?顯然使用未經堆優化的\(\mbox{Dijkstra}\)算法就行啦。

假設咱們已經求出了一個最短路已知的點集,顯然須要向外擴展出一個最近點。一個直觀的想法是,咱們讓小\(A\)和小\(B\)分別求出一個\(pair(u,d)\)表示在本身手上的這張圖上,距離目前已知點集的最近點是\(u\),其距離爲\(d\),而後兩人交換一下信息後就能夠知道最近點到底是誰。

因爲傳遞\(u\)須要\(11\)\(\mbox{bits}\),傳遞\(d\)須要\(9\)\(\mbox{bits}\),而\(\frac{58000}{N}\approx29\),所以咱們在每次擴展出一個點的過程當中大約能夠發送\(2\)\(d\)\(1\)\(u\)。不難構造出以下策略:小\(A\)先告訴小\(B\)他本身求出的\(d_A\),小\(B\)在收到小\(A\)發來的\(d_A\)後轉手發過去一個\(d_B\),而後兩人中\(d\)值較小者向對方發送本身\(u\)便可。

在通訊開始前小\(B\)須要使用一個\(\mbox{bits}\)喚醒小\(A\)開始通訊,而在每輪擴展新點結束後,小\(A\)能夠作到自行展開新一輪的通訊,所以總字節數爲\(29(N-1)+1\)能夠經過本題。

#include"Azer.h"
#include<vector>
using namespace std;

namespace{
    int n,mxdis,cnt,fir,sec,ans;vector<int>dis,vis;
    vector<vector<pair<int,int> > >E;
    pair<int,int>findnxt(){
        pair<int,int>res=make_pair(mxdis+511,n);
        for(int i=0;i<n;++i)if(!vis[i])res=min(res,make_pair(dis[i],i));
        res.first-=mxdis;return res;
    }
    void update(int u,int w){
        dis[u]=mxdis=w;vis[u]=1;
        for(auto x:E[u])dis[x.second]=min(dis[x.second],dis[u]+x.first);
    }
}
void InitA(int _n,int m,vector<int>u,vector<int>v,vector<int>w){
    n=_n;dis.resize(n);vis.resize(n);E.resize(n);
    for(int i=0;i<n;++i)dis[i]=1<<30;
    for(int i=0;i<m;++i){
        E[u[i]].push_back(make_pair(w[i],v[i]));
        E[v[i]].push_back(make_pair(w[i],u[i]));
    }
    update(0,0);cnt=-1;
}
void ReceiveA(bool x){
    do{
        if(ans==n-1)return;
        if(cnt==-1){
            pair<int,int>tmp=findnxt();cnt=0;
            for(int i=0;i<9;++i)SendA(tmp.first>>i&1);
        }
        else if(cnt<9){
            fir|=x<<cnt,++cnt;
            if(cnt==9){
                pair<int,int>tmp=findnxt();
                if(fir>tmp.first){
                    for(int i=0;i<11;++i)SendA(tmp.second>>i&1);
                    update(tmp.second,mxdis+tmp.first);fir=0;cnt=-1;++ans;
                }
                else ++cnt;
            }
        }
        else{
            sec|=x<<cnt-10,++cnt;
            if(cnt==21){
                update(sec,mxdis+fir),fir=sec=0,cnt=-1;++ans;
            }
        }
    }while(cnt==-1);
}
vector<int>Answer(){
    vector<int>res(n);
    for(int i=0;i<n;++i)res[i]=dis[i];
    return res;
}
#include"Baijan.h"
#include<vector>
using namespace std;

namespace{
    int n,mxdis,cnt,fir,sec;vector<int>dis,vis;
    vector<vector<pair<int,int> > >E;
    pair<int,int>findnxt(){
        pair<int,int>res=make_pair(mxdis+511,n);
        for(int i=0;i<n;++i)if(!vis[i])res=min(res,make_pair(dis[i],i));
        res.first-=mxdis;return res;
    }
    void update(int u,int w){
        dis[u]=mxdis=w;vis[u]=1;
        for(auto x:E[u])dis[x.second]=min(dis[x.second],dis[u]+x.first);
    }
}
void InitB(int _n,int m,vector<int>u,vector<int>v,vector<int>w){
    n=_n;dis.resize(n);vis.resize(n);E.resize(n);
    for(int i=0;i<n;++i)dis[i]=1<<30;
    for(int i=0;i<m;++i){
        E[u[i]].push_back(make_pair(w[i],v[i]));
        E[v[i]].push_back(make_pair(w[i],u[i]));
    }
    update(0,0);SendB(true);
}
void ReceiveB(bool x){
    if(cnt<9){
        fir|=x<<cnt,++cnt;
        if(cnt==9){
            pair<int,int>tmp=findnxt();
            for(int i=0;i<9;++i)SendB(tmp.first>>i&1);
            if(fir>=tmp.first){
                for(int i=0;i<11;++i)SendB(tmp.second>>i&1);
                update(tmp.second,mxdis+tmp.first);fir=0;cnt=0;
            }
            else ++cnt;
        }
    }
    else{
        sec|=x<<cnt-10,++cnt;
        if(cnt==21){
            update(sec,mxdis+fir),fir=sec=0,cnt=0;
        }
    }
}

Day 3

指定都市 (Designated Cities)

description

一棵\(n\)個節點的樹,每條邊均是雙向的,正反向分別有一個權值。每次你能夠在樹上選\(x\)個點,須要付出的代價是全部知足沿該邊方向走不回頭沒法到達任何一個選定的點的邊的權值之和。有\(Q\)組詢問,每次詢問給出一個\(E_j\),問在\(x=E_j\)的狀況下最小代價是多少。

\(n\le2\times10^5,Q,E_j\le n\)

solution

若選出了一個點集\(S\),那麼在\(S\)的樹上最小連通塊內的邊的雙向權值都不會被取到,其他的邊中遠離連通塊的方向的邊的權值會被取到。注意當\(x=1\)時上述連通塊會退化成一個點。

一個能夠感性理解的結論是,設\(x=i\)時選定的點集爲\(S_i\),那麼當\(i\ge 2\)時,\(S_i\subseteq S_{i+1}\)

因而即可以先作一遍\(O(n)\)的樹形\(dp\)求出\(x=1,2\)時的答案,再每次貪心選擇使代價減小最多的點便可。

#include<cstdio>
#include<algorithm>
using namespace std;
int gi(){
    int x=0,w=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')w=0,ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}
#define ll long long
const int N=4e5+5;
int n,q,to[N],nxt[N],val[N],head[N],pos[N],p1,p2,dfn[N],low[N],id[N],tim,fa[N],vis[N],tmp[N];
ll sum[N],len[N],mx[N<<2],tag[N<<2],ans[N],tot;
int Max(int u,int v){return len[u]>len[v]?u:v;}
void update(int u,int v,ll w){if(w>ans[2])p1=u,p2=v,ans[2]=w;}
void dfs1(int u,int f){
    for(int e=head[u],v;e;e=nxt[e])
        if((v=to[e])!=f){
            len[v]=len[u]+val[e];dfs1(v,u);
            sum[u]+=sum[v]+val[e^1];
        }
}
void dfs2(int u,int f){
    ans[1]=max(ans[1],sum[u]);pos[u]=u;
    for(int e=head[u],v;e;e=nxt[e])
        if((v=to[e])!=f){
            sum[u]-=sum[v]+val[e^1],sum[v]+=sum[u]+val[e];
            dfs2(v,u);
            sum[v]-=sum[u]+val[e],sum[u]+=sum[v]+val[e^1];
            update(pos[u],pos[v],sum[u]+len[pos[u]]+len[pos[v]]-len[u]-len[u]);
            pos[u]=Max(pos[u],pos[v]);
        }
}
void modify(int x,int l,int r,int ql,int qr,int v){
    if(l>=ql&&r<=qr){mx[x]+=v;tag[x]+=v;return;}
    int mid=l+r>>1;
    if(ql<=mid)modify(x<<1,l,mid,ql,qr,v);
    if(qr>mid)modify(x<<1|1,mid+1,r,ql,qr,v);
    mx[x]=max(mx[x<<1],mx[x<<1|1])+tag[x];
}
int findmx(int x,int l,int r){
    if(l==r)return id[l];int mid=l+r>>1;
    return mx[x]==mx[x<<1]+tag[x]?findmx(x<<1,l,mid):findmx(x<<1|1,mid+1,r);
}
void dfs3(int u,int f){
    fa[u]=f;id[dfn[u]=++tim]=u;
    for(int e=head[u],v;e;e=nxt[e])
        if((v=to[e])!=f)tmp[v]=val[e],dfs3(v,u);
    low[u]=tim;modify(1,1,n,dfn[u],low[u],tmp[u]);
}
void add(int u){
    while(u&&!vis[u])modify(1,1,n,dfn[u],low[u],-tmp[u]),vis[u]=1,u=fa[u];
}
int main(){
    n=gi();
    for(int i=1,j=1;i<n;++i){
        int u=gi(),v=gi();
        to[++j]=v;nxt[j]=head[u];tot+=(val[j]=gi());head[u]=j;
        to[++j]=u;nxt[j]=head[v];tot+=(val[j]=gi());head[v]=j;
    }
    dfs1(1,0);dfs2(1,0);dfs3(p1,0);add(p2);
    for(int i=3;i<=n;++i)ans[i]=ans[i-1]+mx[1],add(findmx(1,1,n));
    q=gi();while(q--)printf("%lld\n",tot-ans[gi()]);return 0;
}

ランプ (Lamps)

description

有一個長度爲\(n\)\(01\)序列\(A\),你能夠對其進行若干次操做,每次操做形如:將區間\([l,r]\)內的全部數(變爲\(0\)/變爲\(1\)/取反),求最小的操做次數使序列\(A\)變成序列\(B\)

\(n\le10^6\)

solution

顯然存在一種最優策略知足任意兩次區間覆蓋操做不相交,任意兩次區間取反操做不相交,且全部區間覆蓋操做在區間取反操做以前進行。

若是肯定了區間覆蓋的操做方式,那麼區間異或的操做次數也就隨之肯定了。於是能夠設計\(dp\)狀態,\(f_{i,0/1/2}\)表示處理完前\(i\)位,第\(i\)位沒有進行區間覆蓋操做/進行了區間覆蓋成\(0\)的操做/進行了區間覆蓋成\(1\)的操做,轉移的時候只須要計算全部操做的區間端點數目,最後答案除\(2\)便可。

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
int gi(){
    int x=0,w=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')w=0,ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}
const int N=1e6+5;
int n,f[N][3],cost[3][3]={{0,1,1},{1,0,2},{1,2,0}};char a[N],b[N];
int main(){
    n=gi();scanf("%s%s",a+1,b+1);
    ++n;a[n]=b[n]='0';
    memset(f,63,sizeof(f));
    f[1][0]=(a[1]-'0')^(b[1]-'0');
    f[1][1]=(0^(b[1]-'0'))+1;
    f[1][2]=(1^(b[1]-'0'))+1;
    for(int i=2;i<=n;++i)
        for(int j=0;j<3;++j)
            for(int k=0;k<3;++k){
                int pre=(j&1?0:(j&2?1:a[i-1]-'0'))^(b[i-1]-'0');
                int nxt=(k&1?0:(k&2?1:a[i]-'0'))^(b[i]-'0');
                f[i][k]=min(f[i][k],f[i-1][j]+cost[j][k]+(pre^nxt));
            }
    printf("%d\n",f[n][0]>>1);return 0;
}

時をかけるビ太郎 (Bitaro, who Leaps through Time)

咕了。

Day 4

ケーキの貼り合わせ (Cake 3)

description

\(n\)塊蛋糕,每塊蛋糕有兩個權值\(V_i\)\(C_i\)。你須要選出其中的\(m\)個排成一個圓排列,設排列爲\(p_1,p_2...p_m\)(定義\(p_{m+1}=p_1\)),則收益爲\(\sum_{i=1}^mV_{p_i}-\sum_{i=1}^m|C_{p_i}-C_{p_{i+1}}|\)。求最大收益。

\(n,m\le2\times10^5\)

solution

假設選出了一個肯定的蛋糕集合,如何排列可使減去的權值儘可能少?

有一個明顯的下界是\(2(C_{\max}-C_{\min})\),同時也不難構造出一種方案達到這個下界。

將蛋糕按照\(C_i\)值排序,而後問題變成了:選出一個區間\([l,r]\),收益爲這個區間內\(V_i\)值的前\(m\)大減去\(2(C_r-C_l)\)

\(l\)從小到大,其對應的最優右端點必定單調不降,所以決策單調性便可。

#include<cstdio>
#include<algorithm>
using namespace std;
int gi(){
    int x=0,w=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')w=0,ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}
#define ll long long
const int N=2e5+5;
const int M=4e6+5;
int n,m,o[N],len,ls[M],rs[M],sz[M],rt[N],tot;
pair<int,int>a[N];ll sum[M],ans=-1ll<<60;
void modify(int &x,int l,int r,int p){
    ++tot;ls[tot]=ls[x];rs[tot]=rs[x];sz[tot]=sz[x];sum[tot]=sum[x];
    x=tot;++sz[x];sum[x]+=o[p];if(l==r)return;int mid=l+r>>1;
    p<=mid?modify(ls[x],l,mid,p):modify(rs[x],mid+1,r,p);
}
ll query(int x,int y,int l,int r,int k){
    if(l==r)return 1ll*o[l]*min(k,sz[x]-sz[y]);int mid=l+r>>1;
    if(k<=sz[rs[x]]-sz[rs[y]])return query(rs[x],rs[y],mid+1,r,k);
    else return sum[rs[x]]-sum[rs[y]]+query(ls[x],ls[y],l,mid,k-(sz[rs[x]]-sz[rs[y]]));
}
void solve(int l,int r,int L,int R){
    int mid=l+r>>1,MID;ll v=-1ll<<60;
    for(int i=max(L,mid+m-1);i<=R;++i){
        ll tmp=query(rt[i],rt[mid-1],1,len,m)-(a[i].first-a[mid].first<<1);
        if(v<tmp)v=tmp,MID=i;
    }
    ans=max(ans,v);
    if(l<mid)solve(l,mid-1,L,MID);if(mid<r)solve(mid+1,r,MID,R);
}
int main(){
    n=gi();m=gi();
    for(int i=1;i<=n;++i)a[i].second=o[i]=gi(),a[i].first=gi();
    sort(a+1,a+n+1);sort(o+1,o+n+1);len=unique(o+1,o+n+1)-o-1;
    for(int i=1;i<=n;++i)modify(rt[i]=rt[i-1],1,len,lower_bound(o+1,o+len+1,a[i].second)-o);
    solve(1,n-m+1,m,n);printf("%lld\n",ans);return 0;
}

合併 (Mergers)

description

有一個\(n\)個節點的樹,每一個點上有一個顏色\(c_i\)。定義一次操做爲將樹上全部顏色爲\(x\)的點的顏色改爲\(y\),求至少進行多少次操做後,樹上任意一條樹邊都知足該樹邊將樹分紅的兩個連通塊包含相同的顏色。

\(n\le5\times10^5\)

solution

對於每種顏色,在其樹上最小連通塊內的邊都已經知足要求,能夠直接縮掉。

縮完全部邊後,至關於樹上任意兩點的顏色均不一樣。此時若選擇兩個點便可讓這兩點路徑上的每一條邊知足要求,至關因而要選出最小數目的鏈覆蓋整棵樹(鏈之間能夠有重複部分),所以答案爲葉子節點個數除以\(2\)向上取整。

#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
int gi(){
    int x=0,w=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')w=0,ch=getchar();
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return w?x:-x;
}
const int N=5e5+5;
int n,m,fa[N],dep[N],dsu[N],du[N],ans;
vector<int>E[N],S[N];
int find(int x){return x==dsu[x]?x:dsu[x]=find(dsu[x]);}
void dfs(int u,int f){
    fa[u]=f;dep[u]=dep[f]+1;
    for(int v:E[u])if(v^f)dfs(v,u);
}
void merge(int x,int y){
    x=find(x),y=find(y);
    while(x^y)
        if(dep[x]>dep[y])dsu[x]=fa[x],x=find(x);
        else dsu[y]=fa[y],y=find(y);
}
int main(){
    n=gi();m=gi();
    for(int i=1;i<n;++i){
        int x=gi(),y=gi();
        E[x].push_back(y);E[y].push_back(x);
    }
    dfs(1,0);
    for(int i=1;i<=n;++i)S[gi()].push_back(i),dsu[i]=i;
    for(int i=1;i<=m;++i)if(S[i].size())for(int x:S[i])merge(x,S[i][0]);
    for(int u=1;u<=n;++u)for(int v:E[u])if(find(u)^find(v))++du[find(v)];
    for(int i=1;i<=n;++i)if(i==find(i)&&du[i]==1)++ans;
    printf("%d\n",ans+1>>1);return 0;
}

鉱物 (Minerals)

咕了。

相關文章
相關標籤/搜索