Gokix

一言(ヒトコト)

关注我

眼前一亮的CF题

那些令我拍手叫绝的题

这题实在是太妙了。或许作为 2F 会迷惑很多人,往前放放可能会获得更可观的 Div2 通过数。

首先考虑记 \(x=\gcd(b_1,b_2,...,b_m)\)。那么 \(x\) 可以表示为 \(\sum (q_i \times b_i),q_i \in \mathbb{Z}\),所以我们一定能通过若干次翻转使得其等价于只翻连续 \(x\) 个数。

于是问题转化为有 \(n\) 个整数,可以进行无限次操作,一次操作可以选择连续的 \(x\) 个数变为它的相反数,求最大可能 \(\sum a\)

注意到翻 \(x\) 个数有些麻烦,考虑一个更强的操作。如果我们连续翻转 \([i,i+x],[i+1,i+x+1]\),就相当于我们只翻转了 \(i\)\(i+x+1\) 两个数。由此一来,我们就获得了同时翻相距为 \(x\) 的两个数的操作。

接下来我们将 \(a\) 按照下标对 \(x\) 同余分成 \(x\) 个小序列 \(c_0,c_1,...,c_{n-1}\)。即 \(1,x+1,2x+1...\) 成为一个小序列,\(2,x+2,2x+2\) 成为一个小序列……。对于每个小序列 \(c_i\),相当于可以进行 \(x=2\) 的操作。

然后我们考虑对于每个 \(c_i\) 寻找最优解,目标是让负数剩的尽可能小(不一定是少,是加和小)。我们可以使原来有奇数个负数的序列只剩一个负数,原来有偶数个负数的序列一个负数都不剩。方法是从左向右扫 \(c\),遇到一个负数就将它和它右面那个数翻过来,这样就实现了至多只剩一个负数。如果还剩一个负数,就不断翻它和它左面的那个数,直至负号移到了绝对值最小的那个数上。

但是由于我们用了一个过强的操作保证每个 \(c_i\) 都取到了最优解,我们还需要保证 \(c\) 之间关系最优才能取得全局最优解。\(c\) 之间关系无非就是是否剩下一个负数,也就是每个 \(c_i\) 负数个数的奇偶性。但注意到在 \(a\) 中每进行一次操作,每个 \(c\) 负数个数的奇偶性都会被恰好改变。原来剩下一个负数的 \(c\) 现在不会剩下负数,原来不剩负数的 \(c\) 现在会剩下一个负数。也就是说每个 \(c\) 负数个数的奇偶性一共就两种情况,都统计一下求个最优值。最后拿全局绝对值和减去两倍它即可。

code
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;

long long rd(){char ch=getchar();long long x=0,f=1;while(ch<'0' || ch>'9'){if(ch=='-') f=-1;ch=getchar();}
                        while('0'<=ch && ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}
void wr(long long x){if(x<0){putchar('-');x=-x;}if(x>9) wr(x/10);putchar(x%10+'0');}

const long long N=1e6+1,lim=1e18;
long long t,n,m,a[N+10],x,sum,cnt[2],ans;

int main(){
	long long i,j,u,v;
	t=rd();
	while(t--){
		n=rd();m=rd();
		for(i=1;i<=n;i++){
			a[i]=rd();
		}
		x=rd();m--;
		while(m--){
			x=__gcd(x,rd());
		}
		cnt[0]=cnt[1]=sum=0;
		for(i=1;i<=n;i++) sum+=abs(a[i]);
		for(i=1;i<=x;i++){
			u=lim;v=0;
			for(j=i;j<=n;j+=x){
				if(a[j]<0){
					v++;
				}
				u=min(u,abs(a[j]));
			}
			if(v&1) cnt[1]+=u;
			else cnt[0]+=u;
		}
		ans=sum-2*min(cnt[0],cnt[1]);
		wr(ans),putchar('\n');
	}
	return 0;
}

被坑了还拍手叫绝。

记原序列长度为 \(n\)

想要搞出一个 RBS,有一个明显的必要条件是左括号和右括号个数得相等(同时也说明 \(n\) 得是偶数)。这点我们可以通过统计原序列中的左括号数和右括号数,并用 \(\frac{n}{2}\) 分别减去它们,得到的结果分别记为 \(L,R\)

显然,当 \(L=0\) 或者 \(R=0\),必然只有唯一解。直接特判掉。

接下来让我们回想起一个括号序列是 RBS 的等价表述:对于任意右括号,其之前的右括号个数均小于左括号个数。

而题目又保证了原序列有至少一种合法分配方式,所以把从左往右数前 \(L\)? 替换成 (,后 \(R\)? 替换成 ) 一定是一种合法分配方案。

证明考虑其它方案必然是该方案某几对左右括号互换位置而得,但每互换一对左右括号,在这对左右括号之间的括号前的右括号数就会加 1,而其它位置的括号数目不受影响。所以每互换一对括号都是一个不优操作。从而证明了上述方案为合法分配方案。

上一个分配方案是最不劣的。而判断是否存在多解,我们只需判断次不劣的方案是否合法。这里直接给出次不劣的方案:在最不劣分配方案基础上,将第 \(L\)? 替换为 ),将第 \(L+1\) 个 '?' 替换为 (,其余不变。

证明思想与上一个证明类似,这里不再赘述。

code
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
long long rd(){char ch=getchar();long long x=0,f=1;while(ch<'0' || ch>'9'){if(ch=='-') f=-1;ch=getchar();}
                        while('0'<=ch && ch<='9'){x=x*10+ch-'0';ch=getchar();}return x*f;}
void wr(long long x){if(x<0){putchar('-');x=-x;}if(x>9) wr(x/10);putchar(x%10+'0');}

const long long N=2e5+10;
long long t,n,l,r,L,R,x,y,a[N+10],f[N+10],ct,po,op;
string s;

int main(){
	long long i,j,u,v,gx;
	t=rd();
	while(t--){
		cin>>s;
		n=s.size();
		l=r=x=0;
		for(i=0;i<n;i++){
			if(s[i]=='(') a[i+1]=1,l++;
			else if(s[i]==')') a[i+1]=2,r++;
			else a[i+1]=3,x++;
		}
		if(n&1){
			putchar('N'),putchar('O'),putchar('\n');
			continue;
		}
		y=n/2;
		L=y-l;R=y-r;
		ct=0;
		for(i=1;i<=n;i++){
			if(a[i]==3){
				ct++;
				if(ct==L) po=i;
				if(ct==L+1) op=i;
				if(ct>L) a[i]=4;
			}
		}
		for(i=0;i<=n+1;i++) f[i]=0;
		for(i=1;i<=n;i++){
			if(a[i]==1 || a[i]==3) gx=1;
			else gx=-1;
			f[i]=f[i-1]+gx;
		}
		if(L==0 ||R==0){
			putchar('Y'),putchar('E'),putchar('S'),putchar('\n');
			continue;
		}
		swap(a[po],a[op]);
		for(i=0;i<=n+1;i++) f[i]=0;
		for(i=1;i<=n;i++){
			if(a[i]==1 || a[i]==3) gx=1;
			else gx=-1;
			f[i]=f[i-1]+gx;
		}
		gx=1;
		for(i=1;i<=n;i++){
			if(f[i]<0){
				gx=0;
				break;
			}
		}
		if(!gx){
			putchar('Y'),putchar('E'),putchar('S'),putchar('\n');
			continue;
		}
		else{
			putchar('N'),putchar('O'),putchar('\n');
			continue;
		}
	}
	return 0;
}

I/O 交互

可以说是 gx 基本了解 I/O交互 机制后做的第一道题。

题目思路本身不难,方法也多种多样。一个简单的做法就是先把后放在左上角,然后尝试向下逼王。一旦王向下走了,那就说明王不在后的下一行,后下行。否则我们把后在这一行从左到右扫一遍,王必然在某时向下走。如果王向上走了,那我们再扫一遍。因为王向上走的次数不会超过 \(7\) 次,所以重扫的次数总共也不会超过 \(7\) 次。

用此题来记录一下做交互题时可能出现的一些错误。

  1. TLE

如果确定是程序复杂度不是错的话,可以认为是在交互的过程中出现了自己意想范围之外的状态。

比如在本题中王有可能突破了后的封锁。你以为王在后的下方,但王可能已经跑上去了。然后后就一直在次底线跑,T了。

  1. ILE

如果使用cout<<endl则不需要fflush(out)

如果并非清除缓冲区的锅,还有可能是一直未读入交互库的输出。因此要注意每一次交互一定要有对应输出!

  1. 准备操作

本题需要特殊考虑的事情。

如果可以开始王就在第一行,此时王会向下走一格。注意此时王在后的下一行,后此时向下走会使得王冲出包围圈。

可以先把后放在 \((1,8)\),再走到 \((1,1)\),开始正常交互。

code
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;

long long t,x,y,p,pp,cnt;
string s;

int main(){
    long long i,j,u,v;
    cin>>t;
    while(t--){
        x=1,y=8;
        cout<<x<<" "<<y<<endl;fflush(stdout);
        cin>>s;
        if(s=="Done") continue;
        x=1,y=1;
        cout<<x<<" "<<y<<endl;fflush(stdout);
        p=0;pp=0;cnt=1; 
        while(s!="Done"){
            cin>>s;
            if(s=="Done"){p=1;break;}
            if(s[0]!='D' && pp){y=1;pp=0;cnt=0;}
            else{
                if(s[0]=='U'){
                    if(y!=1) y=1,cnt=1;
                    else y=8,pp=1;
                }
                if(s[0]=='D'){x++;if(y!=1) pp=1;}
                if(s[0]=='L' || s[0]=='R'){
                	if(y<8) y++;
                	cnt++;
                	if(cnt==8){
                		x++;
                		if(y!=1) pp=1;
					}
				}
            } 
            cout<<x<<" "<<y<<endl;fflush(stdout);
            if(x==7) break;
        }
        if(p) continue;
        cin>>s;
        if(s=="Done") continue;
        if(y!=1) {y=1,cout<<x<<" "<<y<<endl;fflush(stdout);}
        else {y=2,cout<<x<<" "<<y<<endl;fflush(stdout);}
        while(s!="Done"){
            cin>>s;
            if(s=="Done"){p=1;break;}
            y++;
            cout<<x<<" "<<y<<endl;fflush(stdout);
        }
    }
    return 0;
}

一道不要想多题……

与或和定理:\(a+b=a \operatorname{and} b + a \operatorname{or} b\)

我们选定 \(1\) 号点为基准点,拿它和所有剩余节点与一遍或一遍,就求出 \(1\) 号点和其它所有节点各自的和来了。然后注意到我们手中还有 \(2\) 次机会,我们拿这 \(2\) 次机会询问出 \(2\) 号节点和 \(3\) 号节点的和。此时我们已知 \(1,2,3\) 号节点两两的和,不难求出 \(1\) 号节点的值,从而复原整个数列。

code
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;

long long n,k,a[10010];

int main(){
    long long i,j,u,v;
    cin>>n>>k;
    for(i=2;i<=n;i++){
        cout<<"and "<<1<<" "<<i<<endl;
        cin>>u;
        cout<<"or "<<1<<" "<<i<<endl;
        cin>>v;
        a[i]=u+v;
    }
    cout<<"and "<<2<<" "<<3<<endl;
     cin>>u;
    cout<<"or "<<2<<" "<<3<<endl;
    cin>>v;
    a[1]=(a[2]+a[3]-u-v)/2;
    for(i=2;i<=n;i++) a[i]-=a[1];
    sort(a+1,a+n+1);
    cout<<"finish "<<a[k]<<endl;
    return 0;
}

E1 是有 \(n+2\) 此次询问机会,随便搞一下就行了。虽然 E2 只是比 E1 少了一次询问机会,但做法完全不同。

本题的关键点在于值域很小,只有 \([0,n-1]\)

先钦定 \(a_1\) 去 xor 上剩下的数,记作 \(c_{[1,n-1]}\)。现在我们要用仅剩的 2 次机会去找出 \(a_1\) 的值。

这个值域正好是排列的值域,所以可以分类讨论:

  1. 如果 \(a\) 是一个长度为 \(n\) 的一个排列,那么 \(a_i\) 两两不同。则必然可以找到 \(c_x=1\),其除最后一位都与 \(a_i\) 相同。也必然可以找到一个 \(c_y=2\),其最后一位与 \(a_i\) 相同。查询二者与 \(a_1\) 的或值即可还原 \(a_1\)

  2. 如果 \(a\) 不是一个长度为 \(n\) 的排列,那么至少有一对 \((i,j),i<j\) 使得 \(a_i=a_j\)。这时候又有两种情况:

    1. \(i=1\),则必然 \(\exists c_j=0\)

    2. \(i \ne 1\),则必然 \(\exists i,j\) 使得 \(c_i=c_j\)。所以记录一下每个 \(c\) 上一次出现的位置,然后边遍历边看看当前的 \(c\) 在之前有没有出现过就好了。

    找出 \(i,j\) 后只需要查一下二者的 or 就可以求出其值,然后根据 \(c\) 值就可以逆推出 \(a_1\)

code
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;

long long n,c[1000000],per,ans[1000000],las[1000000],zkw,a1;

long long A(long long u,long long v){
	cout<<"AND "<<u<<" "<<v<<endl;
	long long gx;cin>>gx;
	return gx;
}
long long O(long long u,long long v){
	cout<<"OR "<<u<<" "<<v<<endl;
	long long gx;cin>>gx;
	return gx;
}
long long X(long long u,long long v){
	cout<<"XOR "<<u<<" "<<v<<endl;
	long long gx;cin>>gx;
	return gx;
}

int main(){
	long long i,j,u,v;
	cin>>n;
	for(i=2;i<=n;i++){
		c[i]=X(1,i);
	}
	per=1;
	for(i=2;i<=n;i++){
		if(c[i]==0){
			u=1;v=i;
			per=0;
			break;
		}
	} 
	for(i=2;i<=n;i++){
		if(las[c[i]]){
			u=las[c[i]];v=i;
			per=0;
			break;
		}
		las[c[i]]=i;
	}
	if(per){
		for(i=2;i<=n;i++){
			if(c[i]==1){
				zkw=O(1,i)/2*2;
				break;
			}
		}
		for(i=2;i<=n;i++){
			if(c[i]==2){
				zkw+=(O(1,i)%2);
			}
		}
		a1=zkw;
	}
	else{
		j=O(u,v);
		a1=j^c[u];
	}
	cout<<"! "<<a1;
	for(i=2;i<=n;i++){
		cout<<" "<<(c[i]^a1);
	}
	cout<<endl;
	return 0;
}
posted @ 2022-07-23 22:05  Gokix  阅读(60)  评论(0编辑  收藏  举报