2026gdsy秋季集训

9.3

T1

树形dp哈哈。

首先考虑如何刻画方案数,发现这个可以算在每个点上,对于每个点选择不同的边构成的序列是一一对应的,所以考虑计数每个点在哪条边取到的方案数。设状态\(f_{u,0/1/2}\)分别表示考虑u子树内,u在做到u的父边之前就已经染色/在做父边的时候染色/在做父边后仍未被染色的方案数。首先枚举每个给u染色的出边来转移0/2,

\[f_{u,2[w_v>w_u]} \gets f_{v,2}\prod_{w_x<w_v}f_{x,0/1}\prod_{w_x>w_v}f_{x,0,2} \]

2还有一种情况就是不染色,那么就是

\[f_{u,2}\gets \prod_v f_{v,0/1} \]

然后1的情况就是

\[f_{u,1}=\prod_{w_v<w_u} f_{v,0/1}\prod_{w_v>w_u}f_{v,0/2} \]

这个显然可以用前后缀积优化维护。

T2

分成两问解决。

opt1

首先如果x在区间外面直接输出就行,然后分类讨论x有可能是前k大,那么肯定就是在最右边,如果x的左边比它大的个数小于等于k,那么每次肯定会被往左推k步,如果都不满足,那么可以通过有一个结论,做k步之后一定会变成原来第k个1的位置-k,这些都可以对建序列主席树对值域可持久化维护出现次数就行了。

opt2

排序后的前i位是原来数组前i+k位的前i小,那么第i位肯定是小于等于第i小的,我们发现它只有两种取值,第一种是第i小,第二种是原来数组的a[i+k],首先a它肯定不能大于第i小,而且不能大于a[i+k],如果大于a[i+k]也会被换掉,而且它一定可以取到最值。

Code

#include<bits/stdc++.h>
#define lm ((l+r)>>1)
#define rm (lm+1)
using namespace std;
const int N=6e5+5,M=N*23;
int n,q,o,tot,a[N],b[N],c[M],rt[M],ls[M],rs[M];
void build(int &k,int l,int r){k=++tot;if(l==r)return;build(ls[k],l,lm),build(rs[k],rm,r);}
void update(int &k,int l,int r,int x){
	if(l>x||r<x)return;
	++tot,ls[tot]=ls[k],rs[tot]=rs[k],c[tot]=c[k]+1,k=tot;
	if(l==r)return;
	update(ls[k],l,lm,x),update(rs[k],rm,r,x);
}
int calc(int k,int l,int r,int s,int t){
	if(l>t||r<s)return 0;
	if(l>=s&&r<=t)return c[k];
	return calc(ls[k],l,lm,s,t)+calc(rs[k],rm,r,s,t);
}
int find(int k,int l,int r,int x){
	if(l==r)return l;
	int y=c[ls[k]];
	if(y>=x)return find(ls[k],l,lm,x);
	return find(rs[k],rm,r,x-y);
}
int query(int u,int v,int l,int r,int x){
	if(l==r)return l;
	int y=c[ls[v]]-c[ls[u]];
	if(y>=x)return query(ls[u],ls[v],l,lm,x);
	return query(rs[u],rs[v],rm,r,x-y);
}
int main(){
	freopen("sort.in","r",stdin);
	freopen("sort.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>q>>o;
	for(int i=1;i<=n;i++)cin>>a[i],b[a[i]]=i;
	build(o==1?rt[n+1]:rt[0],1,n);
	if(o==1)for(int i=n;i;i--)update(rt[i]=rt[i+1],1,n,b[i]);
	else for(int i=1;i<=n;i++)update(rt[i]=rt[i-1],1,n,a[i]);
	while(q--){
		int l,r,k,x;
		cin>>l>>r>>k>>x;
		if(o==1){
			if(b[x]<l||b[x]>r)cout<<b[x];
			else{
				int y=calc(rt[x],1,n,l,r);
				if(y<=k)cout<<r-y+1;
				else if(calc(rt[x],1,n,l,b[x])>k)cout<<b[x]-k;
				else cout<<find(rt[x],1,n,calc(rt[x],1,n,1,l-1)+k+1)-k;
			}
		}
		else{
			if(x<l||x>r)cout<<a[x];
			else{
				int y=query(rt[l-1],rt[min(r,x+k-1)],1,n,x-l+1);
				if(x+k<=r)y=min(y,a[x+k]);
				cout<<y;
			}
		}
		cout<<'\n';
	}
}

T3

首先大于和小于没区别,所以就变成(1-等于)/2,然后两个集合的和相等有一个trick,可以把另一个集合变成负数然后和为零。显然负数不好做,直接右边全部加m,把值域统一,改到[0,m],所以说就变成了2n值域在[0,m]的数的和是nm。考虑这个上界的限制还是不好做,所以直接考虑把上界容斥掉,直接枚举超过m的个数,然后钦定每个数减掉m+1,剩下的插板法就行了,式子就是\(\sum_{i=0}^{nm/(m+1)}(-1)^i\binom{2n}{i}\binom{nm-i(m+1)+2n-1}{2n-1}\),时间复杂度\(O(nm+tn)\)。

T4

考虑查询的节点会被什么贡献,首先肯定在这个节点最后一次下传之后才会有贡献,然后最后一次下传到该节点一定是最后一次刚好改到该节点,在这个区间内的所有修改都有贡献,那么答案就是这两个时间内与询问节点有交的所有修改的和,再变成f(r)-f(l),询问就变成了每次问前i个修改操作对于某个区间的贡献,我们发现对于所有包含当前节点的的区间,我们询问这个区间的任何一个点都是对的,然后对于相交的,最后肯定能减掉,所以说离线下来扫描线就行。问题就变成了如何去找到每个点最晚的被下传的时间,注意到一个节点标记被下传的条件是修改区间与这个节点有交且不包含这个节点,有两种情况,修改区间的端点在节点内部(不含端点),修改区间的左/右端点是节点右/左端点,那么修改的时候单点改然后查询区间取max就行了。

Code

#include<bits/stdc++.h>
#define ls (k<<1)
#define rs (ls|1)
#define lm ((l+r)>>1)
#define rm (lm+1)
#define pb push_back
using namespace std;
const int N=2e5+5;
int n,m,tot,a[N],L[2*N],R[2*N],fa[2*N],tl[N],tr[N],mx[4*N],t[N],ans[N];
vector< pair<int,int> > g[N],h[N];
struct node{int o,l,r,x;}q[N];
void build(int k,int l,int r){
	tot++,fa[tot]=k,k=tot,L[k]=l,R[k]=r;
	if(l<r){
		int m;cin>>m;
		build(k,l,m),build(k,m+1,r);
	}
	h[l].pb({r,k});
}
void update(int k,int l,int r,int x,int y){
	if(l>x||r<x)return;
	mx[k]=y;
	if(l==r)return;
	update(ls,l,lm,x,y),update(rs,rm,r,x,y);
}
int query(int k,int l,int r,int s,int t){
	if(l>t||r<s)return 0;
	if(l>=s&&r<=t)return mx[k];
	return max(query(ls,l,lm,s,t),query(rs,rm,r,s,t));
}
int calc(int l,int r){return max({query(1,1,n,l+1,r-1),tl[r],tr[l]});}
void add(int x,int y){for(int i=x;i<=n;i+=i&-i)t[i]+=y;}
int ask(int x){int y=0;for(int i=x;i;i-=i&-i)y+=t[i];return y;}
int main(){
	freopen("oracle.in","r",stdin);
	freopen("oracle.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	build(0,1,n);
	for(int i=1;i<=m;i++){
		cin>>q[i].o>>q[i].l>>q[i].r;
		if(q[i].o==2)cin>>q[i].x;
	}
	for(int i=1;i<=m;i++){
		int l=q[i].l,r=q[i].r;
		if(q[i].o==1){
			int k=lower_bound(h[l].begin(),h[l].end(),make_pair(r,0))->second;
			g[fa[k]?calc(L[fa[k]],R[fa[k]]):i].pb({i,1});
			if(l<r)g[calc(l,r)].pb({i,-1});
			else ans[i]+=a[l];
		}
		else update(1,1,n,l,i),update(1,1,n,r,i),tl[l]=tr[r]=i;
	}
	for(int i=1;i<=m;i++){
		if(q[i].o==2)add(q[i].l,q[i].x),add(q[i].r+1,-q[i].x);
		for(auto [x,o]:g[i])ans[x]+=o*ask(q[x].l);
	}
	for(int i=1;i<=m;i++)if(q[i].o==1)cout<<ans[i]<<'\n';
}

9.5

T1

反悔贪心。题目可以转化为每次选两个不交的区间匹配,问最大匹配数。对所有按照左端点从小到大排序,如果当前线段未被匹配,那么就给当前区间找到左端点最小的未被匹配的合法线段进行匹配;如果当前线段已被匹配,那么需找到一个左端点最小的线段替代当前线段进行匹配,然后再给当前线段找匹配线段。这两种操作做完之后答案都能加一,可以证明这是对的。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
int n,m,mch[N],ans[N];
struct node{int l,r,i,j;}a[N];
bool operator <(node x,node y){return x.l!=y.l?x.l<y.l:(x.r!=y.r?x.r<y.r:x.i<y.i);}
bool cmp(node x,node y){return x.r!=y.r?x.r<y.r:x.l<y.l;}
set<node> s;
int main(){
	freopen("project.in","r",stdin);
	freopen("project.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i].l>>a[i].r,a[i].i=i;
	sort(a+1,a+n+1);
	for(int i=1;i<=n;i++)a[i].j=i,s.insert(a[i]);
	for(int i=1;i<=n&&m;i++){
		bool f=1;
		if(!mch[i]){
			auto j=s.lower_bound({a[i].r+1,0});
			if(j==s.end())f=0;
			else ans[a[i].i]=ans[j->i]=m--,mch[j->j]=i,s.erase(j),s.erase(a[i]);
		}
		else{
			int j=mch[i];
			auto x=s.lower_bound({a[i].r+1,0});
			if(x==s.end())f=0;
			else{
				node tmp=*x;s.erase(x);
				x=s.lower_bound({a[j].r+1,0});
				if(x==s.end())f=0,s.insert(tmp),s.insert(a[i]);
				else ans[x->i]=ans[a[j].i],ans[a[i].i]=ans[tmp.i]=m--,mch[tmp.j]=i,mch[x->j]=j,s.erase(x);
			}
		}
	}
	for(auto i=s.begin();i!=s.end()&&m;i++)ans[i->i]=m--;
	for(int i=1;i<=n;i++)cout<<ans[i]<<' ';
}

T2

首先答案肯定是一条从上到下的链,然后往左右各有一个分叉。image
然后这个东西可以先预处理处每个点到四个方向的距离,然后最后一次从上方跑最短路多两维0/1记录一下当前是否往左往右分叉过,然后如果用优先队列跑有可能常数过大,注意到最后答案一定小于\(W(n+m)\),所以说直接改为对于每个dis开vector维护然后用一个变量维护当前下标就行了。

T4

(约定被取下的是白点,没被取下的是黑点)
借鉴wjr的BIT做法,但他说做法的容易推广有点抽象,我以一种更好理解的方法说明(通过构造函数来维护修改tag),这样实现的细节比较好处理。首先我们需要求一个点最近的白色祖先。考虑树剖在每一个重链维护一个set。查询时在每一个重链上跳,如果找到一个合法的重链就在这个重链找前驱就行了,由于只有找一次前驱并且一共只会跳log次重链,所以复杂度是\(O(\log n)\)的。然后第一问是动态删/加点,然后维护连通块大小。然后可以构造函数\(f_u\)表示以u为根的联通块大小(白点包含在儿子的连通块),那么删/加点操作就是当前点到上一个白点的链上全部减/加当前连通块大小。然后对于第二问,还是动态删/加点,不过多了连通块加。连通块加是因为我们可以维护每个点答案的绝对值,因为每次操作所有点的绝对值变化是相同的,这样只需要最后答案统计的时候加上符号就行了。我们还是一样通过构造函数维护,函数\(g_x\)表示x的真实值减去父亲的真实值,如果当前点是白点,被减数就为0。然后这样删/加点就相当于加/减当前点父亲的真实值,然后连通块加就是顶点加,最后求答案的时候把所有点改成黑点就行了,这些直接用树剖做是\(O(q\log^2 n)\)的,但是根据经典结论,链操作可以差分成根链操作,根链操作可以对偶成子树操作,子树操作直接在dfs序上做就行了,由于是单点查/改,所以可以用树状数组维护做到\(O(q\log n)\),瓶颈在set。

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=1e5+5,p=360;
int n,q,tot,h[N],sz[N],fa[N],top[N],dep[N],dfn[N],mp[N];
bool a[N];
vector<int> g[N];
set<int> s[N];
struct BIT{
	int t[N];
	void add(int x,int y){if(x)for(;x<=n;x+=x&-x)t[x]+=y;}
	int ask(int x){int y=0;for(;x;x-=x&-x)y+=t[x];return y;}
	void add1(int x,int y){add(dfn[x],y);}
	void add2(int x,int y){add1(x,y),add(dfn[x]+sz[x],-y);}
	void add3(int x,int z,int y){add1(x,y),add1(fa[z],-y);}
	int ask1(int x){return ask(dfn[x]);}
	int ask2(int x){return ask(dfn[x]+sz[x]-1)-ask(dfn[x]-1);}
	int ask3(int x,int z){return ask1(x)-ask1(fa[z]);}
}T1,T2;
int C(int u){return dep[u]&1?-1:1;}
void dfs1(int u,int f){
	sz[u]=1,fa[u]=f,dep[u]=dep[f]+1;
	for(int v:g[u])if(v!=f){
		dfs1(v,u),sz[u]+=sz[v];
		if(sz[v]>sz[h[u]])h[u]=v;
	}
}
void dfs2(int u,int f){
	dfn[u]=++tot,mp[tot]=u,top[u]=f;
	if(h[u])dfs2(h[u],f);
	for(int v:g[u])if(v!=fa[u]&&v!=h[u])dfs2(v,v);
}
int find(int u){
	int v=h[u];
	while(u){
		int x=top[u];
		if(!s[x].empty()&&*s[x].begin()<=dfn[u]){
			auto i=--s[x].upper_bound(dfn[u]);
			if(*i==dfn[u])return v;
			return h[mp[*i]];
		}
		v=top[u],u=fa[v];
	}
	return 1;
}
void op1(int x){
	int z=fa[find(x)];
	T1.add3(fa[x],z,-T1.ask2(x)),a[x]=1,T2.add2(x,T2.ask3(fa[x],z));
	s[top[x]].insert(dfn[x]);
}
void op2(int x){
	int z=fa[find(fa[x])];
	T1.add3(fa[x],z,T1.ask2(x)),a[x]=0,T2.add2(x,-T2.ask3(fa[x],z));
	s[top[x]].erase(dfn[x]);
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		g[u].pb(v),g[v].pb(u);
	}
	dfs1(1,0),dfs2(1,1);
	for(int i=1;i<=n;i++)T1.add3(i,i,sz[i]);
	cin>>q;
	while(q--){
		int o,x,y;
		cin>>o>>x;
		if(o==1)op1(x);
		if(o==2)op2(x);
		if(o==3){
			cin>>y;int z=find(x);
			cout<<T1.ask2(z)*y<<'\n',T2.add2(z,C(x)*y);
		}
	}
	for(int i=1;i<=n;i++)if(a[i])op2(i);
	int ans=0;
	for(int i=1;i<=n;i++)ans+=(C(i)*T2.ask3(i,1)%p+p)%p;
	cout<<ans;
}

9.15

T1

首先考虑答案是什么,就是\(\max h*\sum w\),其中h和w可以互换,这个max显然不太好做,那么就先扫描线从小到大枚举max,这样对于两边都小于等于max的肯定竖着放,对于有一边大于max的只能横着放,然后两边都大于的显然不能发,那么每个数的状态都是确定的,这样用一个multiset维护最小的k个数就行了,扫描线移动的过程就相当于把长边删掉,把小边加入,可能要注意h等于w的情况。

赛时问题

赛时开始看错题了,以为全部都要选,然后恰好k个翻转,实际上这个跟这题差不多,但是浪费了比较多时间

Code

#include<bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;
const int N=1e6+5;
int n,k,sum,w[N],h[N];
vector<int> g[N];
multiset< int,greater<int> > s;
void ins(int x){
	if(s.size()<k)sum+=x,s.insert(x);
	else if(s.size()==k&&x<*s.begin())sum+=x-*s.begin(),s.erase(s.begin()),s.insert(x);
}
signed main(){
	freopen("canopy.in","r",stdin);
	freopen("canopy.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>h[i],g[w[i]].pb(i);
		if(w[i]!=h[i])g[h[i]].pb(i);
	}
	int ans=1e18;
	for(int i=1;i<=1e6;i++)if(!g[i].empty()){
		for(int x:g[i]){
			if(w[x]>i)ins(w[x]);
			else if(h[x]>i)ins(h[x]);
			else{
				if(w[x]!=h[x]){
					auto it=s.find(max(w[x],h[x]));
					if(it!=s.end())sum-=*it,s.erase(it);
				}
				ins(min(w[x],h[x]));
			}
		}
		if(s.size()==k)ans=min(ans,i*sum);
	}
	cout<<ans;
}

T2

发现同余是不好做的,考虑能否转化为相等,这样就可以使用字符串哈希了,显然可以先给每个a_i模p(我去赛时没想到),这样最后\(a_i+a_j+a_k\)只有\(0,p,2p\)三种取值就做完了。然后先枚举\(a_i和a_j\),但是高精度加法显然超时了,注意到哈希具有可加性,直接哈希值相加就行了,然后\(a_k\)可以用unordered_map找。然后高精取模是简单的,因为99999很特殊,从高到低位\(a_{i-l} \gets a_i\)就是对的了。

T3

首先根据期望的线性性,总的期望等于每一个格子的期望。显然先离散化,设c_i表示第i个墙的最终高度,第i列第x行的答案就是\(P[c_i<x](1-\prod_{j<i}P[c_j<x])(1-\prod_{j>i}P[c_j<x])\),把括号拆了,就变成\(P[c_i<x]-\prod_{j\le i}P[c_j<x]-\prod_{j\ge i}P[c_j<x]+\prod_{j}P[c_j<x]\),显然如果这个式子直接维护就是\(O(n^2)\)的,考虑优化。从下到上扫描线,每一次维护当前行的答案,扫描线移动的时候,会有若干个i的\(P[c_j<x]\)会加上1/2,我们考虑对于上述的4个项分别维护。首先第一项和第四项是好维护的,第二三项实际上就是一个区间覆盖或者后/前缀乘2,然后全局求和,这取决于你当前的\(P[c_j<x]\)是0还是1/2,这个如果用线段树暴力维护tag是可以做的,不过可能实现细节比较多,时间复杂度\(O(n\log n)\)。显然这很不优雅,注意到我们最后本质上答案等价于极大的非0前后缀的答案,那我们可以把零全部改成一,这样区间赋值就改成了后缀乘1/2,用一个指针来维护当前极大的非0前后缀在哪就行了,在仙人掌上常数较大,需要用标记永久化化卡常,记得离散化求答案要乘上。

赛时记录

由于之前见过n次这道题,而且我个人一直以为非常困难,所以赛时一直不敢开这题,然后发现是比较好做的,但是已经时间不够了,而且我没有想到后面的优化,暴力维护tag细节太多了,所以赛时没有调过。

Code

#include<bits/stdc++.h>
#define int long long
#define pb push_back
#define ls (k<<1)
#define rs (ls|1)
#define lm ((l+r)>>1)
#define rm (lm+1)
using namespace std;
const int N=5e5+5,M=N*4,p=1e9+7,inv=5e8+4;
int n,m,a[N],b[N],c[N*2];
bool vis[N];
vector<int> g[N*2];
struct SGT{
	int sum[M],tag[M];
	void pushup(int k){sum[k]=(sum[ls]+sum[rs])%p*tag[k]%p;}
	void build(int k=1,int l=1,int r=n){
		tag[k]=1;
		if(l==r){sum[k]=1;return;}
		build(ls,l,lm),build(rs,rm,r);
		pushup(k);
	}
	void modify(int s,int t,int x,int k=1,int l=1,int r=n){
		if(l>t||r<s)return;
		if(l>=s&&r<=t){sum[k]=sum[k]*x%p,tag[k]=tag[k]*x%p;return;}
		modify(s,t,x,ls,l,lm),modify(s,t,x,rs,rm,r);
		pushup(k);
	}
	int query(int s,int t,int x=1,int k=1,int l=1,int r=n){
		if(l>t||r<s)return 0;
		if(l>=s&&r<=t)return sum[k]*x%p;
		x=x*tag[k]%p;
		return (query(s,t,x,ls,l,lm)+query(s,t,x,rs,rm,r))%p;
	}
}A,B;
signed main(){
	freopen("barrel.in","r",stdin);
	freopen("barrel.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i],c[++m]=a[i];
	for(int i=1;i<=n;i++)cin>>b[i],c[++m]=b[i];
	sort(c+1,c+m+1),m=unique(c+1,c+m+1)-c-1;
	for(int i=1;i<=n;i++)g[lower_bound(c+1,c+m+1,a[i])-c].pb(i),g[lower_bound(c+1,c+m+1,b[i])-c].pb(i);
	int ans=0,sum=0,L=1,R=n;
	A.build(),B.build();
	for(int i=1;i<m;i++){
		sum+=g[i].size();
		for(int x:g[i]){
			if(!vis[x])vis[x]=1,A.modify(x,n,inv),B.modify(1,x,inv);
			else A.modify(x,n,2),B.modify(1,x,2);
		}
		while(vis[L])L++;
		while(vis[R])R--;
		ans=(ans+((sum*inv%p-A.query(1,L-1)-B.query(R+1,n)+!R*A.query(n,n)*n%p)%p+p)%p*(c[i+1]-c[i])%p)%p;
	}
	for(int i=1;i<=n;i++)ans=ans*2%p;
	cout<<ans;
}

T4

匹配问题考虑建图hall定理。\(a_i\)连向\(b_i\),\(b_i\)向所有满足条件的\(a_j\)。这样子如果我们找到了一个完美匹配,再加上\(a_i\)连上\(b_i\)的边,就有一个对应的环分解。那么根据hall定理,考虑构造一个反例\(|N(S)|<|S|\),那么\(|N(S)|\)显然只跟最小的b有关系,所以说\(|N(S)|=\sum_i[b_{min}<a_i]-1\),因为我们不考虑\(a_{min}\)和\(b_{min}\)的边,并且一定不会有\(b_i\)能到\(a_{min}\),因为如果有这种b一定会把限制放宽。那么枚举这个\(b_{min}\),那么对一个固定的\(|N(S)|\),显然\(S\)越大越好,那么\(S\)就是所有\(b_i>b_{min}\)的b。那么最后合法的条件就是\(\forall i,1+\sum_j[a_i<b_j] \le \sum_j[b_i<a_j]-1\),移项变成\(2 \le \sum_j[b_i<a_j]-\sum_j[a_i<b_j]\),惊奇的发现就是右边就是相交的区间个数,所以原题的充要条件就变成每个\([a_i,b_i]\)区间都有相交的区间。考虑怎么快速维护这个东西,设\(L/R_i\)为i左/右边第一个和它相交的区间,这个东西可以在值域上扫描线区间覆盖区间求max做。然后对于每个询问,就变成了l到r必须包含\(L_i\)和\(R_i\)的其中一个。考虑对下标扫描线,在线段树上维护还有哪些i的\(R_i\)未被包含的\(L_i\)的最小值,然后修改就是单点删除和区间求最小值,然后就做完了,时间复杂度\(O(n\log n)\)

Code

#include<bits/stdc++.h>
#define ls (k<<1)
#define rs (ls|1)
#define lm ((l+r)>>1)
#define rm (lm+1)
using namespace std;
const int N=5e5+5,M=N*8;
int n,m,q,a[N],b[N],L[N],l[N],r[N],ans[N];
basic_string<int> g[N],h[N];
namespace SGT{
	int mx[M],tag[M];
	void adt(int k,int x){mx[k]=tag[k]=x;}
	void pushup(int k){mx[k]=max(mx[ls],mx[rs]);}
	void pushdw(int k){int &x=tag[k];if(x)adt(ls,x),adt(rs,x),x=0;}
	void build(int k,int l,int r){
		tag[k]=0;
		if(l==r){mx[k]=n-L[l];return;}
		build(ls,l,lm),build(rs,rm,r);
		pushup(k);
	}
	void modify(int k,int l,int r,int s,int t,int x){
		if(l>t||r<s)return;
		if(l>=s&&r<=t){adt(k,x);return;}
		pushdw(k);
		modify(ls,l,lm,s,t,x),modify(rs,rm,r,s,t,x);
		pushup(k);
	}
	int query(int k,int l,int r,int s,int t){
		if(l>t||r<s)return 0;
		if(l>=s&&r<=t)return mx[k];
		pushdw(k);
		return max(query(ls,l,lm,s,t),query(rs,rm,r,s,t));
	}
}
using namespace SGT;
int main(){
	freopen("duel.in","r",stdin);
	freopen("duel.out","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n,m=2*n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)cin>>b[i];
	for(int i=1;i<=n;i++)L[i]=query(1,1,m,b[i],a[i]),modify(1,1,m,b[i],a[i],i);
	memset(mx,0,sizeof(mx)),memset(tag,0,sizeof(tag));
	for(int i=n;i>=1;i--)g[n+1-query(1,1,m,b[i],a[i])]+=i,modify(1,1,m,b[i],a[i],n+1-i);
	build(1,1,n);
	cin>>q;
	for(int i=1;i<=q;i++)cin>>l[i]>>r[i],h[r[i]]+=i;
	for(int i=1;i<=n;i++){
		for(int x:g[i])modify(1,1,n,x,x,0);
		for(int x:h[i])ans[x]=n-query(1,1,n,l[x],i)>=l[x];
	}
	for(int i=1;i<=q;i++)cout<<(ans[i]?"Yes":"No")<<'\n';
}

9.20

T1

这个操作过于麻烦了,所以说想着怎么简化这个操作。发现这个交换很麻烦,考虑转换成两个数交换,手玩一下就会发现下面这个图。image
这样我们就通过3次操作将a,b交换了。然后我们发现做一次1,再做一次n,序列不变,所以说只用考虑a,b操作次数的奇偶性。如果n和m至少有一个奇数,那么我们就可以对奇数的数组做n次1,这样数组还是不会变,然后奇偶性就对了。最后如果全是偶数一定无解,考虑奇偶性。

T2

由于操作可逆,考虑中转。转化成一个排列做n次到0~n-1,注意到贪心交换的最坏情况已经是n次了,所以大胆猜测每次一定能换一个。所以说我们假设归位x,那么设\(p_y=x\),那么原式就转化为了\(p_x \oplus x \le p_y \oplus y\),那么用堆维护最小的x就做完了。

T3

首先颜色数<=k非常好做,因为只会操作1*1,然后>k的猜测操作数很少,实际上最多两次。考虑构造,首先第一步我们钦定是删左上角,且能删越多越好(钦定删掉的部分的颜色跟其他部分相同),然后再从上一个正方形的右下角往左上角删,这样第一次可以删1个,后面都能删两个,但是可能奇偶性不对,注意到两个正方形的颜色可以不同,所以就做完了。image
那么只用判定是否一次能删光,那么就是原正方形挖掉一个正方形后的颜色数要为k或k-1,首先枚举长度,然后判断以(i,j)为左上角的正方形能删掉多少个,考虑每一个颜色的贡献,这个正方形一定可以覆盖到所有的这个颜色,对应的(i,j)的范围可以算出来,所以预处理是矩形覆盖,查询是单点查,直接二位差分就行了,时间复杂度\(O(n^3)\)。

T4

注意到值域很奇怪,不妨转化为 \(1\le i-a_i \le n\),考虑图论建模,将\(i\)连上\(i-a_i\),这显然是一棵外向基环树,然后发现取这个环就是对的,因为\(a_i=i-nxt_i\),最后一定会消掉。

T5

考虑逆序对,合法的一个必要条件就是\(\frac{n(n-1)}{2} \equiv 0\pmod 2\),即\(n\equiv 0,1 \pmod 2\),通过构造证明它是充分的。考虑增量构造,那我们可以先做完n-4,然后考虑[1,n-4]与[n-3,n]的贡献,首先我们可以一直操作(n-3,i),将1到n-3向右循环位移,变成n-3,1,2......n-4,n-2,n-1,n,然后将1到n-2向左循环位移,就变成1,2,.....,n-4,n-2,n-3,n-1,n,将n-2和n-3交换了,然后对n-1和n也一样做,最后就变成1,2,...,n-4,n-2,n-3,n,n-1,然后做(n-3,n),(n-2,n-1),(n-3,n-1),(n-2,n)就还回去了。

Code

#include<bits/stdc++.h>
using namespace std;
int n;
void out(int x,int y){cout<<x<<' '<<y<<'\n';}
void upd(int x){
	for(int i=1;i<=n-4;i++)out(i,x-1);
	out(x-1,x);
	for(int i=n-4;i>=1;i--)out(i,x);
}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	if(n%4>1)cout<<"NO";
	else{
		cout<<"YES\n";
		for(;n>1;n-=4)upd(n-2),upd(n),out(n-3,n),out(n-2,n-1),out(n-3,n-1),out(n-2,n);
	}
}
posted @ 2026-09-03 16:37  Lqs314  阅读(13)  评论(1)    收藏  举报