0129题解

~ 2026-1-29 12:00:29

T1 倒水

source:瞎编的,个人感觉难度<1800<1800

直觉告诉我,这个过程应该是这样的:

定义操作5为:操作(1,2,4)若干次,然后此时的水都倒进桶里。

那么应该是:操作5执行若干次,然后再执行若干次,把此时一部分水倒进桶里。

对拍之后发现是对的。

于是我们定义cost[x]cost[x]表示最少几次,能让三个杯子恰好凑够xx的水,且都倒进了桶里,cost2[x]cost2[x]表示最少几次,我能让三个杯子中一部分水恰好是xx,且都倒进了桶里。

显然costcost随时可以用,cost2cost2只有最后一次可以用。

那么我们就可以开心dp了,没有细节。

复杂度O(xyz+N(x+y+z))O(xyz+N(x+y+z))

#include<bits/stdc++.h>
#define maxn 105*105*105
#define ll long long
using namespace std;
int x,y,z,mx;
int qx[maxn],qy[maxn],qz[maxn],cost[maxn],cost2[maxn],ans[maxn];
int head,tail;
int dis[102][102][102];
int dp[maxn];
void ins(int x,int y,int z,int d)
{
	if (dis[x][y][z]!=-1) return;
	dis[x][y][z]=d;
	tail++; qx[tail]=x; qy[tail]=y; qz[tail]=z;
}
void check(int x,int d)
{
	if (cost2[x]==-1) cost2[x]=d;
	cost2[x]=min(cost2[x],d);
} 
int main()
{
	cin>>x>>y>>z>>mx;
	head=1,tail=0;
	memset(dis,-1,sizeof(dis));
	memset(cost,-1,sizeof(cost));
	memset(cost2,-1,sizeof(cost2));
	ins(0,0,0,0);
	while (head<=tail)
	{
		int X=qx[head],Y=qy[head],Z=qz[head],d=dis[X][Y][Z]; head++;
		//倒干净 
		if (cost[X+Y+Z]==-1) cost[X+Y+Z]=d+(X!=0)+(Y!=0)+(Z!=0);
		cost[X+Y+Z]=min(cost[X+Y+Z],d+(X!=0)+(Y!=0)+(Z!=0));
		//只是倒出去一部分 
		check(X,d+1); check(Y,d+1); check(Z,d+1); check(X+Y,d+2); check(X+Z,d+2); check(Y+Z,d+2);
		ins(x,Y,Z,d+1); ins(X,y,Z,d+1); ins(X,Y,z,d+1);
		ins(0,Y,Z,d+1); ins(X,0,Z,d+1); ins(X,Y,0,d+1);
		int tt;
		tt=min(X,y-Y); ins(X-tt,Y+tt,Z,d+1);
		tt=min(X,z-Z); ins(X-tt,Y,Z+tt,d+1);
		tt=min(Y,x-X); ins(X+tt,Y-tt,Z,d+1);
		tt=min(Y,z-Z); ins(X,Y-tt,Z+tt,d+1);
		tt=min(Z,x-X); ins(X+tt,Y,Z-tt,d+1);
		tt=min(Z,y-Y); ins(X,Y+tt,Z-tt,d+1);	
	}
	memset(dp,-1,sizeof(dp));
	memset(ans,-1,sizeof(ans));
	dp[0]=0;
	for (int i=0;i<mx;i++)
	if (dp[i]!=-1)
	{
		for (int j=1;j<=x+y+z;j++)
		if (cost[j]!=-1 && i+j<=mx)
		{
			if (dp[i+j]==-1) dp[i+j]=dp[i]+cost[j];
			dp[i+j]=min(dp[i+j],dp[i]+cost[j]);
			//倒一次 
			if (ans[i+j]==-1) ans[i+j]=dp[i]+cost[j];
			ans[i+j]=min(ans[i+j],dp[i]+cost[j]);
		}
		for (int j=1;j<=x+y+z;j++)
		if (cost2[j]!=-1 && i+j<=mx)
		{
			//倒一部分 
			if (ans[i+j]==-1) ans[i+j]=dp[i]+cost2[j];
			ans[i+j]=min(ans[i+j],dp[i]+cost2[j]);
		}
	}
	for (int i=1;i<=mx;i++) cout<<ans[i]<<" "; cout<<endl;
}

T2 联通

SOURCE:CF1706E

部分分做法略。这题简单死了。

第一种做法就是重构树了。

我们根据操作时间建立一棵重构树,在重构树上求出iii+1i+1最早连通的时间aia_i

那么,对于每次询问,答案就是max([ali,ari1])\max([a_{l_i},a_{r_i-1}])

其实我想让大家练练整体二分的。

我们也可以通过整体二分来求aia_i

一个超级好写的方式是,我们动态开点建一棵线段树,每个节点存当前答案在[l,r][l,r]的询问有哪些。

然后直接跑loglog遍模拟,每一遍模拟的过程中,一旦合并到询问[l,r][l,r]对应的一半的时候,就把所有询问算一下,看看应该往左丢还是往右丢就行。

复杂度多一个loglog,但是不是每道题都会多这个loglog的。

需要注意的一个细节是,你不能递归建树,只能bfs的形式来建树,因为这样你编号的顺序才是按层数编号

#include<bits/stdc++.h>
#define maxn 200005
#define ll long long
using namespace std;
int n,m,q;
int x[maxn],y[maxn],qx[maxn],qy[maxn];
int l[maxn<<2],r[maxn<<2],lson[maxn<<2],rson[maxn<<2],mid[maxn<<2],ans[maxn];
basic_string<int> query[maxn<<2];
int fa[maxn];
int find(int x) {return x==fa[x]?x:fa[x]=find(fa[x]);}
void merge(int x,int y) {x=find(x); y=find(y); if (x==y) return; fa[x]=y;}
int zd[maxn<<2];
#define LS (now<<1)
#define RS ((now<<1)|1)
#define MID ((l+r)>>1)
void build(int now,int l,int r)
{
	if (l==r) {zd[now]=ans[l]; return;}
	build(LS,l,MID); build(RS,MID+1,r);
	zd[now]=max(zd[LS],zd[RS]);
}
int get(int now,int l,int r,int L,int R)
{
	if (L>R) return 0;
	if (L<=l && r<=R) return zd[now];
	int ret=0;
	if (L<=MID) ret=max(ret,get(LS,l,MID,L,R));
	if (MID+1<=R) ret=max(ret,get(RS,MID+1,r,L,R));
	return ret;
}

void work()
{
	cin>>n>>m>>q;
	for (int i=1;i<=m;i++) cin>>x[i]>>y[i];
	for (int i=1;i<=q;i++) cin>>qx[i]>>qy[i];
	l[1]=1; r[1]=m; mid[1]=(1+m)>>1;
	int now=1,tot=1;
	while (now<=tot)
	{
		if (l[now]==r[now]) {now++; continue;}
		int ls=++tot,rs=++tot;
		lson[now]=ls; rson[now]=rs;
		l[ls]=l[now]; r[ls]=mid[now]; mid[ls]=(l[ls]+r[ls])>>1;
		l[rs]=mid[now]+1; r[rs]=r[now]; mid[rs]=(l[rs]+r[rs])>>1;
		now++;
	}
	
	for (int i=1;i<n;i++) query[1]+=i;
	now=1;
	while (now<=tot)
	{
		for (int i=1;i<=n;i++) fa[i]=i;
		for (int i=1;i<=m;i++)
		{
			merge(x[i],y[i]);
			if (i==mid[now])
			{
				if (l[now]!=r[now])
					for (int id:query[now])
						if (find(id)==find(id+1)) query[lson[now]]+=id;
						else query[rson[now]]+=id;
				else
					for (int id:query[now]) ans[id]=mid[now];
				now++;
			}
		}
	}	
	build(1,1,n-1);
	for (int i=1;i<=q;i++) cout<<get(1,1,n-1,qx[i],qy[i]-1)<<" "; cout<<endl;
}
int main()
{
	ios::sync_with_stdio(false); cin.tie(0);
	work();
}

T3 通信网络

对于一个部分分做法,我们只需要枚举每个点作为中继点zz,然后二分答案,dfsdfs一下每棵子树,看看子树内有多少个在范围内的点即可。

这个查询过程显然可以通过按dfs序建立的主席树来加速,可以做到两个loglog

一个loglog的做法也很明显:我们直接把这个点当根的时候,对应的所有主席树都拿出来,然后从这些主席树的根节点处同时二分,这样复杂度就只有一个loglog了。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

const int MAXN=3e5+5;
struct edge{
    int to,next;
}e[MAXN<<1];
int head[MAXN],cnt,n,in[MAXN],out[MAXN],ind,fa[MAXN],w[MAXN],k,N;
int tot,ls[MAXN<<6],rs[MAXN<<6],s[MAXN<<6],rt[MAXN],mx,rev[MAXN];
int lm[MAXN],rm[MAXN],bd[MAXN],pos[MAXN],res[MAXN];

template<typename T>inline void read(T&num){
    int f=1,ch=getchar();num=0;
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)){num=num*10+(ch^48);ch=getchar();}
    num=num*f;
}
template<typename T>void print(T x){
    if(x<0){putchar('-');x=-x;}
    if(x>9)print(x/10);
    putchar(x%10+48);
}

ll F(int x){return 1ll*x*(x-1)/2;}
inline void add(int u,int v){
    e[++cnt].to=v;e[cnt].next=head[u];head[u]=cnt;
}
#define mid ((l+r)>>1)
inline void push_up(int x){
    s[x]=s[ls[x]]+s[rs[x]];
}
void ins(int pre,int&now,int l,int r,int p){
    if(!now)now=++tot;
    if(l==r){s[now]=s[pre]+1;return;}
    if(p<=mid){rs[now]=rs[pre];ins(ls[pre],ls[now],l,mid,p);}
    else{ls[now]=ls[pre];ins(rs[pre],rs[now],mid+1,r,p);}
    push_up(now);
}
#undef mid

void predfs(int u,int f){
    in[u]=++ind;fa[u]=f;rev[ind]=u;
    for(int i=head[u];i;i=e[i].next){
        int y=e[i].to;if(y==f)continue;
        predfs(y,u);
    }out[u]=ind;
}

int main(){
//	freopen("networksample1.in","r",stdin);
//	freopen("network.out","w",stdout);
    read(n);read(k);
    for(int i=1;i<=n;i++){
        read(w[i]);mx=max(mx,w[i]);
    }
    N=mx+1;
    for(int i=1;i<n;i++){
        int u,v;read(u);read(v);add(u,v);add(v,u);
    }
    predfs(1,0);
    for(int i=1;i<=n;i++){
        ins(rt[i-1],rt[i],1,N,w[rev[i]]);
    }
    for(int i=1;i<=n;i++){
        int cnts=0,L=rt[in[i]-1],R=rt[out[i]],ad=0;
        lm[0]=rt[0];rm[0]=rt[n];bd[0]=0;
        for(int j=head[i];j;j=e[j].next){
            int y=e[j].to;if(y==fa[i])continue;
            cnts++;lm[cnts]=rt[in[y]-1];rm[cnts]=rt[out[y]];
            bd[cnts]=0;
        }
        int l=1,r=N;
        while(l<r){
            int mid=(l+r)>>1;
            int al=s[ls[rm[0]]]-s[ls[lm[0]]]+bd[0],now=s[ls[R]]-s[ls[L]]+ad;
            ll all=1ll*now*(al-now);all+=F(now);
//            cerr<<"&"<<al<<" "<<now<<" "<<all<<endl;
            for(int i=1;i<=cnts;i++){
//            	cerr<<"^"<<s[ls[rm[i]]]-s[ls[lm[i]]]+bd[i]<<endl;
                all-=F(s[ls[rm[i]]]-s[ls[lm[i]]]+bd[i]);
            }
//            cerr<<i<<" "<<l<<" "<<r<<" "<<mid<<"*"<<all<<endl;
            if(all<k){
                l=mid+1;
                L=rs[L];R=rs[R];ad=now;
                for(int i=0;i<=cnts;i++){
                    bd[i]+=s[ls[rm[i]]]-s[ls[lm[i]]];
                    lm[i]=rs[lm[i]];rm[i]=rs[rm[i]];
                }
            }
            else{
                r=mid;L=ls[L];R=ls[R];
                for(int i=0;i<=cnts;i++){
                    lm[i]=ls[lm[i]];rm[i]=ls[rm[i]];
                }
            }
        }l--;
//        cerr<<i<<" "<<l<<endl;
        if(l==mx)res[i]=1000000;
        else res[i]=l-w[i];
    }
    int ans=1e9;
    for(int i=1;i<=n;i++){
        ans=min(ans,res[i]);
    }
    print(ans);putchar('\n');
    return 0;
}

T4 3sum

SOURCE:arc180D

部分分做法略。

这题当T4其实有点简单的(但是去年CSP/NOIP的T4好像还不如这个难)。

我们考虑对于每组询问,区间内的最大值显然是不可避免的要被选到的。

除了最大值所在位置xx以外(如果有多个,可以钦定最左/最右),一共就这么三种情况:

(1)选了[L+1,R1][L+1,R-1],以及L,RL,R两个端点。

(2)选了[L,t1],[t,t],[t+1,R][L,t-1],[t,t],[t+1,R]三个区间。

为啥只有这两种形式呢?

我们考虑假设t<xt<x,那么,[t+1,R][t+1,R]的最大值一定是AxA_x[L,t][L,t]需要划分成两段区间,无论第二段区间是什么,我都可以疯狂把它右端点归给[t+1,R][t+1,R]这一段,依旧不会改变[t+1,R][t+1,R]的最大值。

所以一定只有上述两种情况。

第一种情况非常容易处理,不说了。

第二种情况,我们可以分ttxx左边还是右边分别做。其实只用做一边,另一边就是翻转序列/询问再做一遍。

以下用ttxx右边来设计算法:

我们考虑建立一个笛卡尔树。

那么每一个位置ii,都有一个管辖范围[li,ri][l_i,r_i]

我们对于询问[L,R],x[L,R],x,想找的分割位置,实际上就是满足:

  • x<iRx<i\leq R
  • riRr_i\geq R

的所有ii中,Ai+min(li,i1)A_i+min(l_i,i-1)的最小值。

对于每一个ii,我们可以在建笛卡尔树的过程中预处理出ri,Ai+min(li,i1)r_i,A_i+min(l_i,i-1)

关键是,怎么对于每一组询问去求答案。

考虑扫描线。

我们按照iinn11的顺序来枚举。

首先,把所有rj=ir_j=i的位置jj,插入到线段树中。在线段树中的下标是jj,值是Aj+min(lj,j1)A_j+min(l_j,j-1)

然后,我们枚举所有R=iR=i的询问。此时,线段树中存储的所有元素,都满足rjRr_j\geq R,我们就只用关心x<jRx<j\leq R一个限制了,直接在线段树中查询区间[x+1,R][x+1,R]的最小值即可。

时间复杂度O((q+n)logn)O((q+n)logn),常数有点大。

#include <bits/stdc++.h>
#define maxn 250005
#define ls (now<<1)
#define rs (now<<1|1)
#define mid ((l+r)>>1)
#define ll long long
#define mod 1000000007
using namespace std;
int zd[maxn<<2],zx[maxn<<2];
int n,q;
int a[maxn],ans[maxn];
struct nod{
	int l,r,id,qid;
}Q[maxn];
void build(int now,int l,int r)
{
	if (l==r) {zd[now]=zx[now]=l; return;}
	build(ls,l,mid); build(rs,mid+1,r);
	if (a[zd[ls]]>=a[zd[rs]]) zd[now]=zd[ls]; else zd[now]=zd[rs];
	if (a[zx[ls]]<=a[zx[rs]]) zx[now]=zx[ls]; else zx[now]=zx[rs];
}
//找区间最大值所在id 
int query(int now,int l,int r,int L,int R)
{
	if (L<=l && r<=R) return zd[now];
	int ret=0;
	if (L<=mid)
	{
		int id=query(ls,l,mid,L,R);
		if (ret==0 || a[id]>a[ret]) ret=id;
	}
	if (mid+1<=R)
	{
		int id=query(rs,mid+1,r,L,R);
		if (ret==0 || a[id]>a[ret]) ret=id;
	}
	return ret;
} 
//找区间最小值所在id
int query2(int now,int l,int r,int L,int R)
{
	if (L<=l && r<=R) return zx[now];
	int ret=0;
	if (L<=mid)
	{
		int id=query2(ls,l,mid,L,R);
		if (ret==0 || a[id]<a[ret]) ret=id;
	}
	if (mid+1<=R)
	{
		int id=query2(rs,mid+1,r,L,R);
		if (ret==0 || a[id]<a[ret]) ret=id;
	}
	return ret;
} 

int R[maxn],ansR[maxn]; //管辖右端点,以及答案 
void get(int l,int r)
{
	int id=query(1,1,n,l,r); //找到区间最大值
//	cout<<l<<" "<<r<<" "<<id<<endl;
	if (id==l) ansR[id]=1<<29;
	else ansR[id]=a[id]+a[query2(1,1,n,l,id-1)]; //找到你左边最小值。
	R[id]=r; //你最多管到r
	if (l<=id-1) get(l,id-1); 
	if (id+1<=r) get(id+1,r); 
}

int zx2[maxn<<2],lazy[maxn<<2]; //第二棵线段树只要最小值
int query3(int now,int l,int r,int L,int R)
{
	if (L>R) return 1<<29;
	if (L<=l && r<=R) {return zx2[now];}
	if (lazy[now]) 
	{
		zx2[ls]=zx2[rs]=1<<29;
		lazy[ls]=lazy[rs]=1;
		lazy[now]=0;
	}
	int ret=1<<29;
	if (L<=mid) ret=min(ret,query3(ls,l,mid,L,R));
	if (mid+1<=R) ret=min(ret,query3(rs,mid+1,r,L,R));
	return ret;
}
void ins(int now,int l,int r,int pos,int val)
{
	if (l==r) {zx2[now]=min(zx2[now],val); return;}
	if (lazy[now]) 
	{
		zx2[ls]=zx2[rs]=1<<29;
		lazy[ls]=lazy[rs]=1;
		lazy[now]=0;
	}
	if (pos<=mid) ins(ls,l,mid,pos,val);
	else ins(rs,mid+1,r,pos,val);
	zx2[now]=min(zx2[ls],zx2[rs]);
} 

vector<nod> T[maxn];
vector<int> T2[maxn];
void work()
{
	for (int i=1;i<=n;i++) T[i].clear();
	build(1,1,n);
	//case1:中间的最大值+两边两个值 
	//case2:中间的最大值+某一边两个值 
	for (int i=1;i<=q;i++) 
	{
		int id=query(1,1,n,Q[i].l+1,Q[i].r-1); 
		ans[Q[i].qid]=min(ans[Q[i].qid],a[id]+a[Q[i].l]+a[Q[i].r]);
		//找到你的mid所在
		if (a[Q[i].l]>a[id]) id=Q[i].l;
		if (a[Q[i].r]>a[id]) id=Q[i].r;
		
		if (id-2>=Q[i].l)
		ans[Q[i].qid]=min(ans[Q[i].qid],a[id]+a[Q[i].l]+a[Q[i].l+1]);
		if (id+2<=Q[i].r)
		ans[Q[i].qid]=min(ans[Q[i].qid],a[id]+a[Q[i].r]+a[Q[i].r-1]);
		
		Q[i].id=id; //你这个询问最大值所在位置在这里。 
		T[Q[i].r].push_back(Q[i]);
	}
//	for (int i=1;i<=q;i++) cout<<Q[i].l<<" "<<Q[i].r<<" "<<Q[i].id<<endl;
	get(1,n); 
	for (int i=1;i<=n;i++) T2[i].clear();
	for (int i=1;i<=n;i++) T2[R[i]].push_back(i);
//	for (int i=1;i<=n;i++) cout<<i<<" "<<R[i]<<" "<<ansR[i]<<endl;
	//现在开始做第二棵线段树
	lazy[1]=1; zx2[1]=1<<29;
	for (int i=n;i>=1;i--)
	{
		for (int id:T2[i])
		{
			ins(1,1,n,id,ansR[id]);
		}
		for (auto tt:T[i])
		{
			//我要找的是tt以右,>=tt.r,且<=i的最小值
			int val=query3(1,1,n,tt.id+1,tt.r);
			ans[tt.qid]=min(ans[tt.qid],a[tt.id]+val); 
		}
	}
}

void init()
{
	ios::sync_with_stdio(false); cin.tie(0);
	cin>>n>>q;
	for (int i=1;i<=n;i++) cin>>a[i];
	for (int i=1;i<=q;i++) {cin>>Q[i].l>>Q[i].r; Q[i].qid=i;}
	for (int i=1;i<=q;i++) ans[i]=1<<30;
	work();
//	cout<<ans[1]<<endl;
//	return;
	reverse(a+1,a+n+1);
	for (int i=1;i<=q;i++) {Q[i].l=n+1-Q[i].l; Q[i].r=n+1-Q[i].r; swap(Q[i].l,Q[i].r);}
//	for (int i=1;i<=n;i++) cout<<a[i]<<" "; cout<<endl;
//	for (int i=1;i<=q;i++) cout<<Q[i].l<<" "<<Q[i].r<<endl;
	work();
	for (int i=1;i<=q;i++) cout<<ans[i]<<"\n";
}
int main()
{
	init();
} 


我们会审查剪贴板内容,并对发布不合适内容的同学进行相应的处理