JOISC2019 简要题解

第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\)式:

\[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)

咕了。

posted @ 2019-03-23 21:43  租酥雨  阅读(2238)  评论(4编辑  收藏  举报