补题总结

简介

去重八后遗留下来的题。

A. 兔子 Zečevi

题面

首先可以看出:这道题可以二分答案。于是我们将其转化为判定型问题。

已知一只兔子一开始可以跳 \(p_i\) 那么远,那么如果 \(p_i\) 比当前二分的答案 \(mid\) 要大,那显然可以直接跳过。

考虑 \(p_i\lt mid\) 的情况,首先我们先跳 \(p_i\) 步肯定是不劣的。此时的位置就在 \(x_i+p_i\),那第一次进食的范围就在 \([x_i,x_i+p_i]\) 之间,由此我们可以得到第 \(j\) 次进食的范围在 \([x_i,x_i+p_i+j-1]\) 之间。

然后我们会发现这变成了一个匹配问题,可以套用二分图匹配的讨论进去。我们知道二分图匹配有一种贪心策略是对于每个左部点,找到它能匹配的最靠前的右部点进行匹配。换到这个上面就是:当我扫到 \(x_i\) 时,我需要将 \(x_i+p_i,x_i+p_i+1,\dots,x_i+mid-1\) 这些点放进右部点并与左部点连边。如果扫到一个胡萝卜,那么就将它放入左部点连边,然后将它与它能匹配的最靠前的右部点进行匹配。

关于连边,显然这个跟位置先后有关系,所以我们可以用扫描线,维护匹配则可以用线段树维护右部点的匹配情况。

代码:

const int N=1e5+6;
struct seg_tree{
	struct Node{
		int sum,lt,ls,rs;
	}node[N<<7];
	int rt=0,cnt=0;
	void new_node(int &pos)
	{
		pos=++cnt;
		node[pos].sum=node[pos].lt=node[pos].ls=node[pos].rs=0;
	}
	void pushdown(int pos,int &ls,int &rs,int l,int r)
	{
		if(node[pos].lt)
		{
			int mid=l+r>>1;
			if(!ls)
			{
				new_node(ls);
			}
			if(!rs)
			{
				new_node(rs);
			}
			node[ls].sum+=(mid-l+1)*node[pos].lt,node[rs].sum+=(r-mid)*node[pos].lt;
			node[ls].lt+=node[pos].lt,node[rs].lt+=node[pos].lt;
			node[pos].lt=0;
		}
	}
	void pushup(int pos,int ls,int rs)
	{
		node[pos].sum=node[ls].sum+node[rs].sum;
	}
	void update(int &pos,int l,int r,int ql,int qr)
	{
		if(!pos)
		{
			new_node(pos);
		}
		if(l>=ql&&r<=qr)
		{
			node[pos].sum+=(r-l+1);
			node[pos].lt++;
			return;
		}
		pushdown(pos,node[pos].ls,node[pos].rs,l,r);
		int mid=l+r>>1;
		if(ql<=mid)
		{
			update(node[pos].ls,l,mid,ql,qr);
		}
		if(qr>mid)
		{
			update(node[pos].rs,mid+1,r,ql,qr);
		}
		pushup(pos,node[pos].ls,node[pos].rs);
	}
	void del(int pos,int l,int r,int x)
	{
		if(l==r)
		{
			node[pos].sum-=x;
			return;
		}
		pushdown(pos,node[pos].ls,node[pos].rs,l,r);
		int mid=l+r>>1;
		if(node[node[pos].ls].sum>x)
		{
			del(node[pos].ls,l,mid,x);
		}
		else
		{
			del(node[pos].rs,mid+1,r,x-node[node[pos].ls].sum);
			node[pos].ls=0;
		}
		pushup(pos,node[pos].ls,node[pos].rs);
	}
	int query(int pos,int l,int r)
	{
		if(l==r)
		{
			return l;
		}
		pushdown(pos,node[pos].ls,node[pos].rs,l,r);
		int mid=l+r>>1;
		if(node[node[pos].ls].sum)
		{
			return query(node[pos].ls,l,mid);
		}
		else
		{
			return query(node[pos].rs,mid+1,r);
		}
	}
}tr;
struct rac{
	int x,p;
}a[N],b[N];
int n,m,mx;
bool fl;
void insert(int it,int x)
{
	if(a[it].p>=x)
	{
		return;
	}
	tr.update(tr.rt,0,mx,a[it].x+a[it].p,a[it].x+x-1);
}
void erase(int it)
{
	fl&=(tr.query(tr.rt,0,mx)>=b[it].x);
	if(tr.node[tr.rt].sum<=b[it].p)
	{
		tr.rt=0;
	}
	else
	{
		tr.del(tr.rt,0,mx,b[it].p);
	}
}
bool check(int x)
{
	fl=true;
	tr.rt=tr.cnt=0;
	int it1=1,it2=1;
	while(it1<=n&&it2<=m)
	{
		if(a[it1].x<=b[it2].x)
		{
			insert(it1,x);
			it1++;
		}
		else
		{
			erase(it2);
			it2++;
		}
	}
	while(it1<=n)
	{
		insert(it1,x);
		it1++;
	}
	while(it2<=m)
	{
		erase(it2);
		it2++;
	}
	fl&=(tr.node[tr.rt].sum==0);
	return fl;
}
signed main()
{
	n=read(),m=read();
	for(int i=1;i<=n;i++)
	{
		a[i].x=read(),a[i].p=read();
	}
	for(int i=1;i<=m;i++)
	{
		b[i].x=read(),b[i].p=read();
	}
	sort(a+1,a+n+1,[&](rac x,rac y)
	{
		return x.x<y.x;
	});
	sort(b+1,b+m+1,[&](rac x,rac y)
	{
		return x.x<y.x;
	});
	int l=0,r=0,ans=0;
	for(int i=1;i<=n;i++)
	{
		r+=a[i].p;
	}
	for(int i=1;i<=m;i++)
	{
		r+=b[i].p;
	}
	r/=n;
	mx=r+(int)(1e9);
	while(l<=r)
	{
		int mid=l+r>>1;
		if(check(mid))
		{
			ans=mid;
			l=mid+1;
		}
		else
		{
			r=mid-1;
		}
	}
	write(ans);
	return 0;
}

B. Rotating Gears

题面

神仙题,做法过于巧妙以至于我想不到。

首先显然我们要对整棵树进行二分图染色,相同颜色的朝同一个方向旋转。

考虑如何维护一个点所在的最大连通块,一种想法是将询问离线下来然后用线段树分治做,据说有这种做法。

我们考虑除了 DSU 之外还有没有别的办法维护一个连通块。一个很简单粗暴的想法是枚举两个点 \(x,y\),然后尝试判断这两个点是否在同一个连通块。

如果 \(x,y\) 是祖先和后代关系,那么我们只要保证 \(x,y\) 的路径上没有被删除的点就行了。如果不是,那么我们可以将问题转化为 \(x,y\) 和 \(\operatorname{LCA}(x,y)\) 是否连通,这个可以用第一种情况的方法做。

所以总而言之:我们需要知道 \(x\) 和它的一个祖先 \(y\) 之间是否有被删除点。这个显然可以用标记的方式,将被删除点做个标记,然后就相当于算树上两点间路径权值的和。

那为了找到和 \(x\) 连通的所有点,我们可以反过来:找到 \(x\) 的所有祖先中与 \(x\) 连通且深度最浅的点,那在这个点以上的所有点和 \(x\) 必然都不连通,那么我们只需要看这个点的子树内有多少点和这个点连通即可。

因此我们可以先树剖,然后把树通过树剖压成一个 dfn 序。因为要求当前节点到祖先节点之间的路径的权值和,所以我们可以维护一个前缀和 \(s\),如果当前节点的前缀和与我们要求的祖先节点的前缀和相同,那么说明这个路径的权值和就是 \(0\)。那如果权值和为 \(0\),我们就可以更新角度。

但是我们怎么知道一段区间里是否有和要求的点连通的点?这个我们可以观察到结论:若区间 \(\operatorname{LCA}\) 不与要求的点连通,那么这段区间必然不存在和要求的点连通的点。这是显然的。

所以我们可以维护每段区间的 \(\operatorname{LCA}\),然后进行判断。相应的,如果我要加点或删点,那么我也只需要在这个点的子树内对前缀和进行修改就行了。

这个东西的理论复杂度是 \(O(n\log^2n)\) 的,可以过,但是还有更优秀的方法。

你会发现我们并不关注 \(\operatorname{LCA}\) 是谁,我们只关注 \(\operatorname{LCA}\) 的前缀和是多少。因此我们维护每段区间的 \(\operatorname{LCA}\) 的前缀和,然后你可以观察到子树内的点集的 \(\operatorname{LCA}\) 必然还在子树内,所以我们只需要多用一个 lazy_tag 进行区间修改就行了。

当然,你也可以选择在线段树上对维护的前缀和做个差分,那么每次修改就不需要标记 lazy_tag。

代码:

const int N=1e5+6,mod=360;
int n,cnt,fa[N],son[N],siz[N],top[N],dfn[N],id[N],col[N];
set<int>s[N];
vector<int>v[N];
struct seg_tree{
	struct Node{
		int l,r,c,lt,cnt;
	}node[N<<2];
	void build(int l,int r,int pos)
	{
		node[pos].l=l,node[pos].r=r,node[pos].c=node[pos].lt=0,node[pos].cnt=r-l+1;
		if(l==r)
		{
			return;
		}
		int mid=l+r>>1;
		build(l,mid,pos<<1);
		build(mid+1,r,pos<<1|1);
	}
	void pushdown(int pos,int ls,int rs)
	{
		if(node[pos].lt)
		{
			if(!node[ls].c)
			{
				node[ls].lt=(node[ls].lt+node[pos].lt)%mod;
			}
			if(!node[rs].c)
			{
				node[rs].lt=(node[rs].lt+node[pos].lt)%mod;
			}
			node[pos].lt=0;
		}
	}
	void pushup(int pos,int ls,int rs)
	{
		node[pos].cnt=0;
		if(!node[ls].c)
		{
			node[pos].cnt+=node[ls].cnt;
		}
		if(!node[rs].c)
		{
			node[pos].cnt+=node[rs].cnt;
		}
	}
	void update(int pos,int l,int r,int s)
	{
		if(node[pos].l>=l&&node[pos].r<=r)
		{
			node[pos].c+=s;
			return;
		}
		pushdown(pos,pos<<1,pos<<1|1);
		int mid=node[pos].l+node[pos].r>>1;
		if(l<=mid)
		{
			update(pos<<1,l,r,s);
		}
		if(r>mid)
		{
			update(pos<<1|1,l,r,s);
		}
		pushup(pos,pos<<1,pos<<1|1);
	}
	int updatea(int pos,int l,int r,int x,int y)
	{
		if(x<0)
		{
			return 0;
		}
		if(node[pos].l>=l&&node[pos].r<=r)
		{
			if(x-node[pos].c)
			{
				return 0;
			}
			node[pos].lt=(node[pos].lt+y)%mod;
			return node[pos].cnt;
		}
		pushdown(pos,pos<<1,pos<<1|1);
		int mid=node[pos].l+node[pos].r>>1,ans=0;
		if(l<=mid)
		{
			ans+=updatea(pos<<1,l,r,x-node[pos].c,y);
		}
		if(r>mid)
		{
			ans+=updatea(pos<<1|1,l,r,x-node[pos].c,y);
		}
		return ans;
	}
	int query(int pos,int x)
	{
		if(node[pos].l==node[pos].r)
		{
			return node[pos].c;
		}
		pushdown(pos,pos<<1,pos<<1|1);
		int mid=node[pos].l+node[pos].r>>1;
		if(x<=mid)
		{
			return query(pos<<1,x)+node[pos].c;
		}
		else
		{
			return query(pos<<1|1,x)+node[pos].c;
		}
	}
	int querya(int pos)
	{
		if(node[pos].l==node[pos].r)
		{
			return (node[pos].lt*col[id[node[pos].l]]%mod+mod)%mod;
		}
		pushdown(pos,pos<<1,pos<<1|1);
		return querya(pos<<1)+querya(pos<<1|1);
	}
}tr;
void dfs1(int x,int f)
{
	fa[x]=f;
	son[x]=0;
	siz[x]=1;
	for(auto i:v[x])
	{
		if(i==f)
		{
			continue;
		}
		col[i]=-col[x];
		dfs1(i,x);
		siz[x]+=siz[i];
		if(siz[i]>siz[son[x]])
		{
			son[x]=i;
		}
	}
}
void dfs2(int x,int t)
{
	top[x]=t;
	dfn[x]=++cnt; 
	id[cnt]=x;
	if(son[x])
	{
		dfs2(son[x],t);
	}
	for(auto i:v[x])
	{
		if(i==fa[x]||i==son[x])
		{
			continue;
		}
		dfs2(i,i);
	}
}
int find(int x)
{
	int la=0;
	while(x&&(s[top[x]].empty()||(*s[top[x]].begin())>dfn[x]))
	{
		la=top[x];
		x=fa[top[x]];
	}
	if(!x)
	{
		return 1;
	}
	auto it=prev(s[top[x]].upper_bound(dfn[x]));
	if(dfn[x]==(*it))
	{
		return la;
	}
	else
	{
		return id[(*it)+1];
	}
}
signed main()
{
	n=read();
	for(int i=1,x,y;i<n;i++)
	{
		x=read(),y=read();
		v[x].push_back(y);
		v[y].push_back(x);
	}
	col[1]=1;
	dfs1(1,0);
	dfs2(1,1);
	tr.build(1,cnt,1);
	int q=read();
	while(q--)
	{
		int op=read();
		if(op==1)
		{
			int x=read();
			s[top[x]].insert(dfn[x]);
			tr.update(1,dfn[x],dfn[x]+siz[x]-1,1);
		}
		else if(op==2)
		{
			int x=read();
			s[top[x]].erase(dfn[x]);
			tr.update(1,dfn[x],dfn[x]+siz[x]-1,-1);
		}
		else
		{
			int x=read(),y=read();
			int up=find(x);
			write(tr.updatea(1,dfn[up],dfn[up]+siz[up]-1,tr.query(1,dfn[up]),(y*col[x]%mod+mod)%mod)*y);
			putchar('\n');
		}
	}
	write(tr.querya(1));
	return 0;
}

C. Plants vs Zombies

题面

纯纯水题,只是有点难写。

显然我们需要知道这只僵尸出现时前面有多少只僵尸(因为前面的僵尸会帮它挡豌豆),因此我们可以按照僵尸出现的时间排序,如果时间一样按索引排序。

考虑当前这只僵尸能存活多久,显然答案具有可二分性,因此我们先二分答案将其转化为一个判定型问题。

再知道答案的情况下,我们很容易算出这只僵尸会受到的地刺的攻击。最难处理的是豌豆,我们会发现只要这只僵尸前面还有僵尸在,那么它就不会受到豌豆射手的伤害,因此我们只关心前面僵尸的最长存活时间以及这只僵尸死后豌豆射手还剩下的豌豆数,我们记为 \(mx\) 和 \(last\)。

然后就是一堆分讨。反正一通乱搞就过了。

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

代码:

const int N=1e5+6;
struct zom{
	int t,hp,id;
}a[N];
struct plt{
	int x,at;
}b[N];
int n,m,V,K,D,mx,last,s[N],ans[N];
vector<int>v;
__int128 calc(int x,int id)
{
	int tim=x-a[id].t+1;
	int ed=min((__int128)(tim*V),(__int128)(1e9));
	int it=upper_bound(v.begin(),v.end(),ed)-v.begin();
	if(x<mx)
	{
		return a[id].hp-s[it];
	}
	else
	{
		return a[id].hp-(s[it]+(__int128)(x-max(mx,a[id].t-1))*K*D+(__int128)(last)*D);
	}
}
bool check(int x,int id)
{
	return calc(x,id)>0;
}
signed main()
{
	n=read(),m=read(),V=read(),K=read(),D=read();
	for(int i=1;i<=n;i++)
	{
		a[i].t=read(),a[i].hp=read();
		a[i].id=i;
	}
	for(int i=1;i<=m;i++)
	{
		b[i].x=read(),b[i].at=read();
		v.push_back(b[i].x);
	}
	sort(v.begin(),v.end());
	v.erase(unique(v.begin(),v.end()),v.end());
	for(int i=1;i<=m;i++)
	{
		b[i].x=lower_bound(v.begin(),v.end(),b[i].x)-v.begin()+1;
		s[b[i].x]+=b[i].at;
	}
	int len=v.size();
	for(int i=1;i<=len;i++)
	{
		s[i]+=s[i-1];
	}
	sort(a+1,a+n+1,[&](zom x,zom y)
	{
		return x.t<y.t||x.t==y.t&&x.id<y.id;
	});
	for(int i=1;i<=n;i++)
	{
		if(a[i].t>mx)
		{
			last=0;
		}
		int l=a[i].t,r=1e18;
		ans[a[i].id]=a[i].t-1;
		while(l<=r)
		{
			int mid=l+r>>1;
			if(check(mid,i))
			{
				ans[a[i].id]=mid;
				l=mid+1;
			}
			else
			{
				r=mid-1;
			}
		}
		ans[a[i].id]++;
		if(ans[a[i].id]<mx);
		else if(ans[a[i].id]==mx)
		{
			int tim=ans[a[i].id]-a[i].t+1;
			int ed=min((__int128)(tim*V),(__int128)(1e9));
			int it=upper_bound(v.begin(),v.end(),ed)-v.begin();
			if(s[it]>=a[i].hp);
			else
			{
				int la=a[i].hp-s[it];
				int need=(la+D-1)/D;
				last-=need;
			}
		}
		else
		{
			int la=calc(ans[a[i].id]-1,i);
			int tim=ans[a[i].id]-a[i].t+1;
			int ed=min((__int128)(tim*V),(__int128)(1e9));
			int it=upper_bound(v.begin(),v.end(),ed)-v.begin(),it1=upper_bound(v.begin(),v.end(),ed-V)-v.begin();
			if(s[it]-s[it1]>=la)
			{
				last=K;
			}
			else
			{
				la-=(s[it]-s[it1]);
				int need=(la+D-1)/D;
				last=K-need;
			}
			mx=ans[a[i].id];
		}
	}
	write(ans+1,ans+n+1);
	return 0;
}
posted @ 2026-09-03 11:16  cwxcplh  阅读(20)  评论(0)    收藏  举报