Codeforces Round 882 (Div. 2)

link

题号:CF1847A~F

A

题意:

给定一个数组 \(\{x_1,x_2,\cdots,x_n\}\) 和一个整数 \(k\),记 \(f(l,r)=\sum_{i=0}^{i < r-l} |x_{l+i}-x_{l+i+1}|\),求将数组划分为 \(k\) 个部分的划分方案,使得对每个部分的 \(f(l,r)\) 之和最小.

题解:

简单题,首先我们注意到,如果将 \(l,l+1\) 隔开,那么 \(|x_l-x_{l+1}|\) 这个数就不会计入答案,那么令 \(b_i=|x_i-x_{i+1}|(i<n)\),那么问题就变为从 \(b\) 中选出最大的 \(k\) 个数减掉。方法很多

        read(n);read(k);int ans=0; 
		for(int i=1;i<=n;i++)read(a[i]);
		for(int i=1;i<n;i++)b[i]=abs(a[i+1]-a[i]);
		sort(b+1,b+n);reverse(b+1,b+n);
		for(int i=k;i<n;i++)ans+=b[i];
		cout<<ans<<"\n";

B

乔纳森正在与迪奥的吸血鬼手下战斗。其中有 \(n\) 个吸血鬼,它们的强度分别为 \(a_1, a_2,\cdots, a_n\)

\((l,r)\) 表示由索引 \(l\)\(r\) 的吸血鬼组成的一组。乔纳森意识到每个这样的组的强度取决于它们的最弱环节,即按位与操作。更具体地说,组 \((l,r)\) 的强度等于 \(f(l,r) =\) \(a_l \ \& \ a_{l+1} \ \& \ a_{l+2} \ \& \cdots \& \ a_r\)。这里,\(\&\) 表示按位与操作。

乔纳森希望能快速击败这些吸血鬼手下,因此他会将吸血鬼分成连续的组,使得每个吸血鬼正好属于一组,并且这些组的强度之和尽量小。在所有可能的分组方式中,他希望找到组数最多的方式。

给定每个吸血鬼的强度,找出在所有可能的分组方式中,拥有最小强度之和的组的最大数量。

题解:

注意到,\(\&\) 肯定是单调不增的。那么显然最小强度之和必定是整个序列 \(\&\) 的值,故本质上问题是从前往后或从后往前找到若干个拼在一起,\(\&\) 和为0的段。

        read(n);int ans=1; 
		for(int i=1;i<=n;i++)read(a[i]);
		k=(1ll<<33ll)-1ll;int cnt=0;
		for(int i=1;i<=n;i++){
			k&=a[i];
			if(k==0){
				k=(1ll<<33ll)-1ll;
				++cnt;
			}
		}
		ans=max(ans,cnt);
		cnt=0,k=(1ll<<33ll)-1ll;
		for(int i=n;i;--i){
			k&=a[i];
			if(!k){
				++cnt;k=(1ll<<33ll)-1ll;
			}
		}
		ans=max(ans,cnt);
		cout<<ans<<"\n";

C

DIO 意识到星尘十字军已经知道了他的位置,并且即将要来挑战他。为了挫败他们的计划,DIO 要召唤一些替身来迎战。起初,他召唤了 $ n $ 个替身,第 $ i $ 个替身的战斗力为 $ a_i $。依靠他的能力,他可以进行任意次以下操作:

  • 当前的替身数量为 \(m\)
  • DIO 选择一个序号 \(i \text{ } ( 1 \le i \le m )\)
  • 接着,DIO 召唤一个新的替身,其序号为 \(m+1\),战斗力为 \(a_{m + 1} = a_i \oplus a_{i + 1} \oplus \ldots \oplus a_m\)。其中,运算符 \(\oplus\) 表示按位异或
  • 现在,替身总数就变成了 \(m+1\)

但对于 DIO 来说,不幸的是,星尘十字军通过隐者之紫的占卜能力,已经知道了他在召唤替身迎战的事情,而且他们也知道初始的 \(n\) 个替身的战斗力。现在,请你帮他们算一算 DIO 召唤的替身的最大可能战斗力(指单个替身的战斗力,并非所有替身战斗力之和)。

题解:

手玩一下,容易发现本质上 \(a_x(x>m)\) 无论怎么选,都必定是 \(a_1\sim a_m\) 的某一个子段的异或值。所以就变成了Trie板子题。

D

<

给出一个长度为 \(n\) 的字符串 \(s\),字符串仅由 01 构成。

给出 \(m\) 个区间 \([l_i,r_i]\) (\(1\le i\le m\),\(1\le l_i\le r_i\le n\)),你需要将字符串 \(s\) 的子段 \([l_i,r_i]\) 依次拼接,得到新的字符串 \(t\)

你可以对字符串 \(s\) 进行操作,每次操作可以交换任意两个字符的位置,注意操作不是实际改变,不会影响后续的询问。定义对于字符串 \(s\)\(f(s)\) 表示最小的操作次数,使得拼接得到的新字符串 \(t\) 的字典序最大。

然后有 \(q\) 次询问,每次询问给出一个位置 \(x_i\),表示将原字符串 \(s\)\(x_i\) 位置取反,注意是实际改变,会影响后续的询问。相应的,\(t\) 字符串也会发生改变。你需要求出每次询问后,\(f(s)\) 的值。

题解:

注意到,这个字典序类似于二进制,满足前一个为1比满足后面所有为1更有效。

那么我们可以从优先级的角度考虑问题,优先级怎么求?倒着用线段树做一次区间覆盖即可(也可以正着用并查集做一次区间覆盖

做完区间覆盖后,将所有值按优先级排序(同色的按下标排序)。并且做一个映射使得可以知道原序列每个下标对应的优先级。

然后,我们再考虑维护答案。记当前序列里有 \(cnt\) 个1。让我们想想,可以任意交换两个数,那么本质上就是求优先级在前 \(cnt\) 的有多少个0了。

这一步可以使用线段树维护区间和。线段树维护优先级数组,当然,与原来的线段树一起就可以了。(我不嫌烦地开了个树状数组)

然后我们用 \(cnt-sum(1,cnt)\) 就可以了。

注意WA on 13的原因:当 \(cnt\ge c\)\(c\) 表示需要被设置为1(优先级不为零的部分)的数的个数,这时候需要用 \(c\) 代替 \(cnt\)。也即 \(ans=\min(c,cnt)-sum(1,\min(c,cnt))\)

启发:遇到明显具有优先满足性质的问题,可以考虑求出每个数的优先级,然后进行操作。

#define N 505050 
int a[N],L[N],R[N];
struct node{
	int l,r,lz;
}t[N<<1];
void build(int x,int l,int r){
	t[x].l=l,t[x].r=r;
	if(l==r)return ;
	int mid=l+r>>1;
	build(lc,l,mid);
	build(rc,mid+1,r);
}
void pushdown(int x){
	if(t[x].lz==0||t[x].l==t[x].r)return ;
	t[lc].lz=t[rc].lz=t[x].lz;t[x].lz=0;
}
void change(int x,int l,int r,int k){
	if(l<=t[x].l&&t[x].r<=r){
		t[x].lz=k;return ;
	}
	if(t[x].l>r||t[x].r<l)return ;
	pushdown(x);
	change(lc,l,r,k);
	change(rc,l,r,k);
}
int find(int x,int pos){
	if(t[x].l==t[x].r)return t[x].lz;
	pushdown(x);
	if(t[lc].r>=pos)return find(lc,pos);
	return find(rc,pos);
}
struct Node{
	int id,x;
	bool operator<(const Node b){
		return x==b.x?id<b.id:x>b.x;
	}
}b[N];int c[N],d[N],mx;
void add(int x,int k){
	while(x<=n){
		d[x]+=k;x+=lowbit(x);
	}
}
int ask(int x){
	int ans=0;
	while(x){
		ans+=d[x];x-=lowbit(x);
	}
	return ans;
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;int m,q;cin>>m>>q;
	for(int i=1;i<=n;i++){
		char x;cin>>x;a[i]=x-'0';
	}
	for(int i=1;i<=m;i++)cin>>L[i]>>R[i];
	build(1,1,n);
	for(int i=m;i;--i)change(1,L[i],R[i],m-i+1);
	for(int i=1;i<=n;i++)b[i].x=find(1,i),b[i].id=i,mx+=(b[i].x!=0);
	sort(b+1,b+n+1);int ans=0,cnt=0;
	for(int i=1;i<=n;i++)c[b[i].id]=i;
	for(int i=1;i<=n;i++)cnt+=a[i];
	for(int i=1;i<=n;i++)if(a[i])add(c[i],1);
	while(q--){
		int x;cin>>x;
		if(!a[x]){
			a[x]=1;++cnt;add(c[x],1);
		}
		else {
			a[x]=0;--cnt;add(c[x],-1);	
		}
		cout<<min(mx,cnt)-ask(min(mx,cnt))<<"\n";
	}
}

E

交互题。 \(n\le 5000\),询问次数不超过 \(5500\)

序列 \(a\) 长为 \(n\),其中 \(1\le a_i\le 4\),每一次询问给出3个 \(i,j,k(i<j<k)\),输入以 \(a_i,a_j,a_k\) 为边长的三角形的面积的平方乘16的结果,特别地,如果构不成三角形,输入0,求这个序列。

题解:

我发现,交互题的核心思想:简化确定步骤 \(\&\) 一次确定多个

注意,有个东西叫海伦-秦九韶公式,也即三角形面积与三边关系:记 \(p=\frac{a+b+c}{2}\),则有 \(S=\sqrt{p(p-a)(p-b)(p-c)}\)

所以 \(16S^2=(4S)^2=(a+b+c)(a+b-c)(a-b+c)(-a+b+c)\)

故,本质上我们是得到了三边的关系式。注意到对于 \(a_i\) 而言,只有 \(4\) 种不同的取值,故上式得到的结果最多只有 \(4^3=64\) 种。

手玩一下,大胆猜想在 \(1\le a\le b \le c\le 4\) 的情况下,所组成三角形的面积是相异的。

面对这种问题,注意到可能性很小,可以打表证明该结论。经过穷举,我们证明了以上猜想。

这是一个很有用的性质:除了0之外,一个三角形的面积单射三边长。我们考虑处理出每个面积对应的三角形三边长。

void init(){
	for(int i=1;i<=4;i++){
		for(int j=i;j<=4;j++){
			for(int k=j;k<=4;k++){
				if(i+j<=k)continue;
				int p=(i+j+k)*(i+j-k)*(i+k-j)*(k+j-i);
				s[p]=(node){i,j,k};
			}
		}
	}
}

然后,我们来考虑怎么确定这些值。

注意到 \(5500-5000=500\),意即大部分数必须一次确定。观察询问次数与 \(n\) 的关系,从中推敲结论

怎么一次确定一个数呢?有且只有我们已知两个数 \(a_i,a_j\),然后我们去询问第三个数 \(a_k\)。怎样的两个数 \(a_i,a_j\) 满足条件?

容易发现,对于一个等腰三角形而言,除非底边是两腰之和,否则必定存在该三角形。

则我们假定 \(a_i=a_j\ge 3\),则无论什么数都可以一次问出(因为必定可以组成三角形,\(a_k\le 6\))。

那么,再考虑一下 \(a_i=a_j=2\)?容易发现,有且仅有 \(4\) 不可以确定,其实返回0也变相说明可以确定了

所以本质而言,除了 \(a_i=a_j=1\) 之外,其他都行。

我们先假定 \(a_i=a_j>1\),考虑寻找到这样一对相等的 \(a_i=a_j\)

根据鸽巢原理,在 \(2\times 4+1=9\) 个数中,必然存在至少三个相等的 \(a\)

这启发我们,当 \(n\ge 9\) 时,取出 \(a_1\sim a_9\),使用穷举法尝试找到一对 \(a_i=a_j=a_k=x\)。这样做询问次数为 \({9\choose 3}=84\) 次。

\(x\ge 2\),则直接全部扫一遍即可。

void get_9(int tag){
	for(int i=0;i<f.size();i++)
		for(int j=i+1;j<f.size();j++)
			for(int k=j+1;k<f.size();k++){
				int x=ask(f[i],f[j],f[k]);
				vis[i][j][k]=x;
				if(x==0)continue;
				if(s[x].a==s[x].c&&n>=9&&(tag==0||s[x].a!=1)){
					id=s[x].a;X1=f[i],Y1=f[j],Z1=f[k];
					ans[X1]=ans[Y1]=ans[Z1]=id;
					return ;
				}
			}
	dfs(0,f.size());//等会解释作用
}
//solve:

        if(id>=2){
			for(int i=1;i<=n;i++){
				if(X1==i||Y1==i||Z1==i)continue;
					int k=ask(X1,Y1,i);
					for(int j=1;j<=4;j++){
						if(k==(id+id+j)*(id+id-j)*j*j){
							ans[i]=j;break;
						}
					}
			}
			cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
			cout.flush();	
		}

\(1\) 真的不行吗?注意到若 \(a_i=a_j=1\),则若询问 \(a_k\) 回复为1,则说明 \(a_k=1\),否则说明 \(a_k\ge 2\)

这样的话,我们将剩下的可以确定 \(a_k\ge 2\) 的数找出9个,就必定可以得到 \(a_{i'}=a_{j'}=a_{k'}\ge 2\)了,然后扫一遍未确定的数就可以了。

询问次数?除了两次找三个相等 \(a\) 值之外,所有的答案都只经过一次询问就确定了,当然不会超5500

但是,如果找不到呢?

我们将原本确定的三个一也插入进去,以防止找不到,若找不到就执行下面的dfs直接 \(O(4^c)\) 暴力枚举确定这些数。
\(Code:\)

int a[N],vis[9][9][9],ans[N];
vector<int>f;
struct node{
	int a,b,c;
}s[N];
int ask(int x,int y,int z){
	if(x>y)swap(x,y);
	if(x>z)swap(x,z);
	if(y>z)swap(y,z);
	cout<<"? "<<x<<" "<<y<<' '<<z<<"\n";cout.flush();
	int res;cin>>res;return res;
}
void init(){
	for(int i=1;i<=4;i++){
		for(int j=i;j<=4;j++){
			for(int k=j;k<=4;k++){
				if(i+j<=k)continue;
				int p=(i+j+k)*(i+j-k)*(i+k-j)*(k+j-i);
				s[p]=(node){i,j,k};
			}
		}
	}
}
int cnt=0;
bool check(){
//	for(int i=0;i<f.size();++i)cout<<a[i]<<" ";cout<<"\n";
	for(int i=0;i<f.size();i++)
		for(int j=i+1;j<f.size();j++)
			for(int k=j+1;k<f.size();k++){
				int x=a[f[i]],y=a[f[j]],z=a[f[k]];
				if(x+y<=z||x+z<=y||y+z<=x){
					if(!vis[i][j][k])continue;
					return 0;
				}
				if(vis[i][j][k]==(x+y+z)*(x+y-z)*(x-y+z)*(-x+y+z))continue;
				return 0;
			}
	for(int i=0;i<f.size();++i)ans[f[i]]=a[f[i]];
	return 1;
}
int X1,Y1,Z1,id;
void dfs(int now,int len){
	if(cnt>1)return ;
	if(now>=len){
		cnt+=check();
		return ; 
	}
	for(int i=1;i<=4;i++){
		a[f[now]]=i;dfs(now+1,len);
	}
	return ;
}
void get_9(int tag){
	for(int i=0;i<f.size();i++)
		for(int j=i+1;j<f.size();j++)
			for(int k=j+1;k<f.size();k++){
				int x=ask(f[i],f[j],f[k]);
				vis[i][j][k]=x;
				if(x==0)continue;
				if(s[x].a==s[x].c&&n>=9&&(tag==0||s[x].a!=1)){
					id=s[x].a;X1=f[i],Y1=f[j],Z1=f[k];
					ans[X1]=ans[Y1]=ans[Z1]=id;
					return ;
				}
			}
	dfs(0,f.size());
}
void repeat(){
	f.clear();
	for(int i=1;i<=n;i++){
		if(X1==i||Y1==i||Z1==i)continue;
		if(ask(X1,Y1,i)!=0){
			ans[i]=1;
		}
		else {
			f.push_back(i);if(f.size()==9)break;
		}
	}	
	f.push_back(X1),f.push_back(Y1),f.push_back(Z1);
	cnt=0;
	get_9(1);
	if(id==1){
		if(cnt!=1)cout<<"! -1\n";
		else {
			cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";//the old time:f.size()<9,else id must be 2 or 3 or 4 
		}
		return ;
	}
	for(int i=1;i<=n;i++){
		if(X1==i||Y1==i||Z1==i)continue;
			int k=ask(X1,Y1,i);
			for(int j=1;j<=4;j++){
				if(k==(id+id+j)*(id+id-j)*j*j){
					ans[i]=j;break;
				}
			}
	}
	cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";cout.flush();
}
void solve(){
	for(int i=1;i<=min(9ll,n);i++)f.push_back(i);
	if(n<9){
		get_9(0);
		if(cnt!=1){
			cout<<"! -1\n";cout.flush();
		}
		else{
			cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
			cout.flush();
		}
		return ;
	}
	else {
		get_9(0);
		if(id>=2){
			for(int i=1;i<=n;i++){
				if(X1==i||Y1==i||Z1==i)continue;
					int k=ask(X1,Y1,i);
						for(int j=1;j<=4;j++){
							if(k==(id+id+j)*(id+id-j)*j*j){
								ans[i]=j;break;
							}
						}
			}
			cout<<"! ";for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
			cout.flush();	
		}
		else repeat();
	}
}
signed main(){
	ios::sync_with_stdio(false);
	cin>>n;
	init();
	solve(); 
}

不得不说,代码有点长,有一点点暴力

F

给定一个无穷长的整数数列的前 \(n\)\(a_1,a_2 \dots a_n\),且对于任意 \(i > n\)\(a_i=a_{i-n}|a_{i-n+1}\).

你需要处理 \(q\) 次询问。每次询问给定一个整数 \(x\),请找出最小的 \(i\) 满足 \(a_i>x\)。如果不存在这样的 \(i\),输出 −1

这问题更有意思了。w直接喜提次劣解,不懂为什么我单log却被卡得那么惨,时限3000ms,我直接2994ms

对于有规律的递推数列问题,可以考虑写个十几项找规律。

我们假定 \(n=5\),以 \(345\) 表示 \(a_3|a_4|a_5\),容易得到:

\[\begin{matrix} 1 & 2 &3 &4 &5\\ 12&23&34&45&512\\ 123&234&345&4512&5123\\ 1234&2345&34512&45123&51234\\ 12345\\ \end{matrix} \]

如果用DP式来说,就是 \(f_{i,j}=f_{i-1,j}|f_{i-1,j+1}(j<n)\)\(f_{i,j}=f_{i-1,j}|f_{i,1}(j=n)\)

容易看出,所求 \(i<n^2\)。我们关注矩阵的行内,列内关系。容易发现以下规律:

  • 每一个 \(a_i\) 都对应原序列的一段区间的或运算值
  • \(a_1\) 外,每一个 \(a_i\) 的对应或区间的末尾都不是1
  • 若破环为链,则第 \(i\) 列的第 \(j\) 个对应区间或区间 \([i,i+j-[i+j\le n]]\)

回到本题,从序列上发现性质之后,就应该在运算上发现性质

结合或运算的性质:\(x|x=x\)。对于二元运算 \(f(a,b)\)(不分顺序),凡是满足 \(f(f(a,b),a)=f(f(a,b),b)=f(a,b)\),就说明具有统计可重性(如或运算,最大值运算,最小值运算等),这时候如果是静态使用,则可以通过ST表迅速维护某一区间的运算和,预处理 \(O(n\log n)\),查询单次 \(O(1)\)

关于或运算还有与运算,都有性质:对于序列 \(b_1\sim b_n\),无论 \(n\) 多大,它的前缀或运算值最多只有 \(\log_2 V\) 个不同的值,前缀与运算值同理

为什么?因为做前缀或/与运算,只有在二进制上0/1不断变1/0,至多变化 \(\log V\) 次。

推论:对于序列 \(b_1\sim b_n\)\(n\) 无限制,则其中每一个子区间或运算最多只有 \(n\log V\) 个不同的结果,与运算同理。

由此,我们可以考虑求出这些值,然后将这 \(n\log V\) 个数按权值自大到小排序,就可以二分出符合条件的区间。此时再做一个前缀最小值,就可求得最小 \(a_i\)

for(int i=1;i<=n;i++){
		b[++tot]=(node){a[i],i};
		solve(i,2,n);
	}
	sort(b+1,b+tot+1);//运算符重载
	for(int i=1;i<=tot;i++)g[i]=min(g[i-1],b[i].id);
	while(q--){
		read(k);if(k>=b[1].x){
			cout<<"-1\n";continue;
		}
		int x=lower_bound(b+1,b+tot+1,(node){k,0})-b-1;//注意这里有个坑点,比大小只能与k有关,不可以与id有关,否则会挂掉的。
		cout<<g[x]<<"\n";
	}

然后我们考虑对于每行怎么求出这些值。

其实是简单的,我们可以对于 每一个值都单独倍增/二分一遍就行。但要特判区间右端点不为 \(n+1\),复杂度 \(O(n\log n\log V)\)

这里我在观看题解后采用的一种更为简单的做法:利用线段树的分治思想,(亦或者可以叫整体二分的某个变种),处理一整段区间,对于整个区间值都相同且已经统计过就直接跳过。

void solve(int now,int l,int r){
	int mid=l+r>>1,x=get(now,now+mid-1);
	if(l==r){
		if(b[tot].x!=x&&now+l-1!=n+1){
			b[++tot]=(node){x,(l-1-(now+l-1>n+1))*n+now};
		}
		return ;
	}
	if(b[tot].x==get(now,now+l-1)&&b[tot].x==get(now,now+r-1))return ;
	solve(now,l,mid);solve(now,mid+1,r);
}

简要分析一下复杂度:递归统计每个答案到底层都要 \(\log n\),而会有 \(\log V\) 个到底层,所以复杂度 \(O(\log n\log V)\)

这样就做完了,复杂度 \(O(n\log n\log V+q\log (n\log V))\)

这种若干次二分化递归解决,是一个不错的办法。

Trick 总结

  • D:如果问题中满足某些限制的数明显具有优先级,那么从优先级的角度考虑问题,求出每个数的优先级后再做处理
  • D:注意答案的上下界
  • E:存在性问题,鸽巢原理
  • E:简化确定步骤
  • E:考虑单射的结论
  • E:大胆猜测,打表求证
  • F:对于有规律的递推数列问题,可以考虑写个十几项找规律
  • F:找矩阵的规律,关注行列对角线等,以及运算方式
  • F:多次倍增跳跃求变化用区间递归实现
posted @ 2023-08-04 19:26  spdarkle  阅读(13)  评论(0编辑  收藏  举报