第18回 日本情報オリンピック 春合宿 オンラインコンテスト (JOISC2019)
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\)練習題?
#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\)次切割時,找到剩下沒拿面包的人的最小切割點即可。這樣貪心的正確性顯然。
#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
交互題。
有一棵樹,保證每個點的度數不超過\(18\)。每次可以詢問三個點\((a,b,c)\),交互庫會返回一個點\(d\)使\(dis(a,d)+dis(b,d)+dis(c,d)\)最小(顯然這樣的\(d\)是唯一的)。你需要在不超過\(40000\)次詢問內還原出樹的形態。
\(n \le 2000\)
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\)式:
其中\(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)
咕了。