MX 暑假集训 7.11

【G2R2018】 R.I.P.

省流:\(100+18+10\)

T1: 麦田

题意

给定一个长度为 \(n\) 的序列 \(b\),对于一个长度为 \(m\) 的序列 \(a\) 定义其权值 \(f(a)\) 为进行以下操作后所有元素之和。

  • 若对于 \(1<i<m\)\(a_i<a_{i-1}\)\(a_i<a_{i+1}\),则同时将所有满足条件的 \(a_i\leftarrow \max(a_{i-1},a_{i+1})\),直到不存在满足条件的 \(i\)

\(q\) 次询问,每次询问给定 \(l,r\),求出 \(f(b[l,r])\)

\(1\le n,q\le 10^6\)

solution

咋还有线性做法,咋这么多人做法比我简单,为啥我要写这么多还这么难调。

首先注意到若相邻两个元素相同,那它们都不可能继续增大,每个位置最多增大一次就会与一个相同元素相邻。

依题意模拟,每次更新完检查相邻两个位置能不能增大。

此时就有 \(O(\sum len)\) 做法了,可以获得 \(50\) 分的高分!

我们进一步观察到,这个增长的顺序似乎不是很重要,无论怎样的顺序增长出来的结果都是一样的,所以另一种做法就是从左向右暴力加入一个元素并检查上一个元素,同样是 \(O(\sum len)\) 的。

对于一个长度为 \(len\) 的区间计算均摊是 \(O(len)\) 的,这个均摊有点难以解决,所以我们不考虑撤销一个位置而只考虑加入一个位置,\(10^6\) 的范围启发我想到了用猫树分治。

对序列分治后,我们考虑跨过中点的询问,先暴力向左右扫计算出两边区间平凡的贡献,再考虑二区间合并就行了。

为了方便,我们从 \(mid\) 向左扫并从 \(mid\) 向右扫,这样合并区间时中间的数只可能变成 \(mid+1\) 的值或 \(mid-1\) 的值。

\(mid+1\)\(mid-1\) 较大的值,若 \(mid-1\) 较大,则 \([mid,x]\) 都会被更新为 \(mid-1\) 的值,令 \(r\) 为最大的 \(j\) 满足 \([mid,j]\) 单调递增,那么 \(x\) 即为 \([mid,j]\) 中最后一个小于 \(mid-1\) 的值,直接做一个二分找到即可,\(mid+1\) 更大也同理。

但是这并不严谨,因为位置 \(x\) 可能被 \(x+1\) 更新为更大的值,需要特判。

这些都可以用指针维护,每次也只需要加入元素,均摊时 \(O(len)\) 的,只有在询问的时候用到二分,所以复杂度是 \(O((n+q)\log n)\) 的。

实现起来有一些细节,还挺难调的。

Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include<bitset>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=1e6+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m;
int a[N],b[N],w1[N],w2[N],op[N];
ll ans[N],s[N];

struct ques{int id,l,r;};
vector<ques> vec[N];

inline void check(int l,int r,int x,vector<int> &v) {
	if (x<=l||x>=r) return;
	if (b[x]<b[x-1]&&b[x]<b[x+1]) v.push_back(x);
}

inline void work(int l,int r,ll &res,vector<int> &tmp) {
	while (!tmp.empty()) {
		vector<int> nxt;
		for (int x:tmp) {
			res-=b[x];
			b[x]=max(b[x-1],b[x+1]);
			res+=b[x];
			check(l,r,x-1,nxt);
			check(l,r,x+1,nxt);
		}
		tmp=nxt;
	}
}

inline ll find1(int L,int R,int k) {
	if (L>R) return 0;
	int l=L,r=R,res=-1;
	while (l<=r) {
		int mid=l+r>>1;
		if (b[mid]<k) res=mid,r=mid-1;
		else l=mid+1;
	}
	if (res==-1) return 0;
	return 1ll*(R-res)*k-s[res]+max(k,b[res-1]);
}

inline ll find2(int L,int R,int k) {
	if (L>R) return 0;
	int l=L,r=R,res=-1;
	while (l<=r) {
		int mid=l+r>>1;
		if (b[mid]<k) res=mid,l=mid+1;
		else r=mid-1;
	}
	if (res==-1) return 0;
	return 1ll*(res-L)*k-s[res]+max(k,b[res+1]);
}

inline void work1(int l,int r) {
	int mid=l+r>>1,now=mid;
	s[mid]=b[mid];
	vector<int> mzk;ll emp=0;
	F(i,mid,l) {
		check(i,mid,i+1,mzk);
		work(i,mid,emp,mzk);
		while (now>i&&b[now]<b[now-1]) {
			s[now-1]=s[now]+b[now-1];
			now--;
		}
		for (ques k:vec[i]) if (op[k.id]==1) ans[k.id]+=find1(now+1,mid,w2[k.id]);
	}
}

inline void work2(int l,int r) {
	int mid=l+r>>1,now=mid;
	s[mid]=b[mid];
	vector<int> mzk;ll emp=0;
	f(i,mid,r) {
		check(mid,i,i-1,mzk);
		work(mid,i,emp,mzk);
		while (now<i&&b[now]<b[now+1]) {
			s[now+1]=s[now]+b[now+1];
			now++;
		}
		for (ques k:vec[i]) if (op[k.id]==2) ans[k.id]+=find2(mid,now-1,w1[k.id]);
	}
}

inline void calc(int l,int r,vector<ques> &q) {
	if (q.empty()) return;
	for (ques k:q) vec[k.l].push_back(k);
	for (ques k:q) vec[k.r].push_back(k);
	f(i,l,r) b[i]=a[i];
	ll resL=0,resR=0;
	int mid=l+r>>1;
	vector<int> tmp;
	F(i,mid,l) {
		resL+=b[i];
		check(i,mid,i+1,tmp);
		work(i,mid,resL,tmp);
		for (ques k:vec[i]) {
			w1[k.id]=b[mid-1];
			ans[k.id]+=resL;
		}
	}
	f(i,mid,r) {
		resR+=b[i];
		check(mid,i,i-1,tmp);
		work(mid,i,resR,tmp);
		for (ques k:vec[i]) {
			w2[k.id]=b[mid+1];
			ans[k.id]+=resR;
			if (k.l==mid) ans[k.id]=resR;
		}
	}
	for (ques k:q) {
		if (k.l==mid) continue;
		ans[k.id]-=b[mid];
		if (w1[k.id]>w2[k.id]) op[k.id]=2;
		else op[k.id]=1;
	}
	f(i,l,r) b[i]=a[i];
	work1(l,r);work2(l,r);
	f(i,l,r) vec[i].clear();
}

inline void solve(int l,int r,vector<ques> tmp) {
	if (tmp.empty()) return;
	if (l==r) {
		for (ques k:tmp) ans[k.id]=a[l];
		return;
	}
	if (r-l==1) {
		for (ques k:tmp) {
			if (k.l==k.r) ans[k.id]=a[k.l];
			else ans[k.id]=a[l]+a[r];
		}
		return;
	}
	int mid=l+r>>1;
	vector<ques> qL,qR,qnow;
	for (ques k:tmp) {
		if (k.r<=mid) qL.push_back(k);
		else if (k.l>mid) qR.push_back(k);
		else qnow.push_back(k);
	}
	calc(l,r,qnow);
	solve(l,mid,qL);
	solve(mid+1,r,qR);
}

int main() {
	read(n);read(m);
	f(i,1,n) read(a[i]);
	vector<ques> st;
	f(i,1,m) {
		int l,r;read(l);read(r);
		st.push_back({i,l,r});
	}
	solve(1,n,st);
	f(i,1,m) printf("%lld\n",ans[i]);
	return 0;
}

赛时的诡异做法说完了,讲讲题解简单的 \(O(n)\) 做法。

对于一个元素,它能否增大取决于它左右两边是否存在能使它增大的元素。

\(L_i\)\(L_i<i\)\(a_{L_i+1},a_{L_i+2},\dots ,a_{i-1},a_i\) 严格单调递增且 \(a_{L_i}>a_i\)

\(R_i\)\(R_i>i\)\(a_i,a_{i+1},\dots ,a_{R_i}\) 严格单调递减且 \(a_{R_i}>a_i\)

\(L_i\)\(R_i\) 均存在,则 \(a_i\) 可以变成 \(\max(a_{L_i},a_{R_i})\)

对于一个询问,我们把区间内所有的 \(a_i\) 先加上,再考虑增长带来的贡献,即对于 \(l\le L_i\le R_i\le r\)\(i\) 加上 \(\max(a_{L_i},a_{R_i})-a_i\),这是个二维数点问题,可以做到 \(O(n\log n)\) 解决。

进一步发现 \(L_i,R_i\) 单调递增,所以做一个前缀和可以 \(O(n)\) 解决。

即对每个位置增长的贡献做前缀和,并维护对每个位置 \(j\) 维护最小的 \(k\) 满足 \(L_k\ge j\) 和最大的 \(k\) 满足 \(R_k\le j\)

时间复杂度 \(O(n+q)\)

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=1e6+10;
constexpr int inf=1e9+10;

inline void read(int &x) {
	x=0;
	char ch=getchar();
	while (ch<48) ch=getchar(); 
	while (ch>=48) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
}

int n,m;
int a[N];
int L1[N],L2[N],R1[N],R2[N];
ll s1[N],s2[N];

int main() {
	read(n);read(m);
	f(i,1,n) {
		read(a[i]),s1[i]=s1[i-1]+a[i];
		L1[i]=L2[i]=0;R1[i]=R2[i]=n+1;
	}
	f(i,1,n) L1[i]=(i==1||a[i-1]>=a[i])?(i-1):L1[i-1];
	F(i,n,1) R1[i]=(i==n||a[i+1]>=a[i])?(i+1):R1[i+1];
	f(i,1,n) {
		s2[i]=s2[i-1];
		if (!(L1[i]>=1&&R1[i]<=n&&a[L1[i]]>a[i]&&a[R1[i]]>a[i])) continue;
		s2[i]+=max(a[L1[i]],a[R1[i]])-a[i];
		R2[L1[i]]=min(R2[L1[i]],i);
		L2[R1[i]]=max(L2[R1[i]],i);
	}
	f(i,2,n) L2[i]=max(L2[i-1],L2[i]);
	F(i,n-1,1) R2[i]=min(R2[i+1],R2[i]);
	while (m--) {
		int l,r;read(l);read(r);
		printf("%lld\n",s1[r]-s1[l-1]+((R2[l]<=L2[r])?(s2[L2[r]]-s2[R2[l]-1]):0));
	}
	return 0;
}

一流の団子職人 (Super Dango Maker)

题意

你有 \(n\) 种颜色的团子,每种颜色的团子有 \(m\) 个,一串团子需要每种颜色恰好一个。

现在有一个 \(n\times m\) 的团子序列,你不知道每个团子的颜色,但是你可以给出一些下标,询问这些下标最多组成多少串团子。

你需要在不超过 \(50000\) 次询问给出 \(m\) 组下标,使得每组下标不重复地构成 \(m\) 串团子。

\(1\le n\le 400,1\le m\le 25\)

solution

小清新交互,比较简单。

考虑 \(m=2\) 时怎么做,我们维护一些下标,最后使它们变成正好一串团子,初始时为全部下标,每次拿出一个位置询问剩下的下标能构成几串团子,如果拿走后能构成串数小于 \(1\) ,那么说明这串团子在当前下标集合中只出现了一次,不能拿走,否则说明至少还剩一个和拿走颜色相同的团子,可以拿走,这样复杂度是 \(O(n)\) 的。

扩展到 \(m\) 很大的情况,考虑分治,将 \(m\) 组团子分成两份 \(\frac{m}{2}\) 组团子,那么和上面的类似,如果拿走后串数小于 \(\frac{m}{2}\),那么不能拿走,否则可以拿走,这样复杂度是 \(O(nm\log m)\) 的。

代码很简单。

Code
#include<cstdio>
#include<algorithm>
#include<vector>
#include "dango3.h"
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)

inline void solve(vector<int> tmp,int len) {
	if (len==1) return Answer(tmp);
	vector<int> vL,vR;
	int mid=(len>>1);
	for (int now=0;now<tmp.size();) {
		int x=tmp[now];
		vector<int> qry=tmp;
		qry.erase(lower_bound(qry.begin(),qry.end(),x));
		if (Query(qry)<mid) {
			vL.push_back(x);
			now++;
		}
		else {
			vR.push_back(x);
			tmp.erase(lower_bound(tmp.begin(),tmp.end(),x));
		}
	}
	solve(vL,mid);
	solve(vR,len-mid);
} 

void Solve(int N,int M) {
	vector<int> st;
	f(i,1,N*M) st.push_back(i);
	solve(st,M);
}

Cat, Fox and Maximum Array Split

题意

有一个长度为 \(n\) 隐藏的数组 \(a\),定义区间 \([l,r]\) 的权值 \(f(l,r)=(r-l+1)*\max\limits_{i=l}^{r}a_i\),即长度乘上其中最大值。

你可以给出 \(l,x\),询问最小的 \(r\) 满足 \(f(l,r)=x\),若没有则返回 \(n+1\)

给定 \(k\),你需要在不超过 \(2n\) 次询问中确定最大的 \(m\) 使得能将数组 \(a\) 恰好分成 \(k\) 个子区间满足每个子区间的权值均为 \(m\)

\(1\le k\le n\le 10^4\)

solution

比上面那题难。

由权值的单调性,我们发现对于 \(l,x\),满足 \(f(l,r)=x\)\(r\) 是唯一的,我们可以设左端点为 \(1\),每次令 \(x\)\(n,2n,3n,\dots\),若返回为 \(n\) 则最大值为当前枚举的 \(x\) 除以 \(n\) 后的值,由于是唯一的,所以我们可以在 \(n\) 次操作内确定整个序列的最大值 \(mx\)

于是我们可以枚举最大值所在区间长度 \(l\),使得划分出的 \(k\) 个区间权值均为 \(mx\times l\),而我们又发现除最大值所在区间外的区间长度都不小于最大值所在区间长度,于是我们枚举的范围就变成了 \(\left\lfloor \frac{n}{k} \right\rfloor\),枚举一个数通过询问暴力向后跳 \(k\) 次检查最后是否到达 \(n\) 即可。

次数为 \(n+\left\lfloor \frac{n}{k} \right\rfloor \times k \le 2n\)

Code
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define qwq Ff472130
#define f(i,l,r) for (int i=l;i<=r;i++)
#define F(i,l,r) for (int i=l;i>=r;i--)
constexpr int N=2e5+10;
constexpr int inf=1e9+10;

inline void read(int &x) {scanf("%d",&x);}
inline int query(int l,int x) {
	printf("? %d %d\n",l,x);
	fflush(stdout);
	int ret;read(ret);
	return ret;
}

inline void answer(int x) {
	printf("! %d\n",x);
	fflush(stdout);
	int ret;read(ret);
}

inline void solve() {
	int n,k;read(n);read(k);
	int mx=0,Lmx=n/k;
	f(i,1,n) if (query(1,n*i)==n) {mx=i;break;}
	F(i,Lmx,1) {
		int now=1,val=i*mx;
		f(j,1,k) {
			int ret=query(now,val);
			if (ret!=n+1) now=ret+1;
			if (now==n+1&&j==k) return answer(val);
			if (ret==n+1||now==n+1) break;
		}
	}
	answer(-1);
}

int main() {
	int T;
	read(T);
	while (T--) solve();
	return 0;
}
posted @ 2026-07-11 17:20  Ff472130  阅读(4)  评论(0)    收藏  举报