学弟欢乐赛 - T3 T4 题解

T3

原题:P12246
一道经典经典的计数题目。
我们考虑如果没有进行交换的总方案数。
因为要求子序列是 xyz,我们先从 y 开始考虑。我们假设这个 y 前面有 \(cnt1\) 个x,后面有 \(cnt2\) 个,根据乘法原理,我们很容易知道这个 y 的贡献值是 \(cnt1 \times cnt2\),然后再把每一个y的贡献加起来即可。
我们使用 \(pre_i\) 表示第i个位置前面有多少个 x,\(suf_i\) 表示第i个位置后面有多少个 z,使用 \(O(n)\) 的前缀和、后缀和即可。
\(sum\) 为此时的答案。
现在考虑交换。
我们发现,一共会有 \(3 * 3 = 9\) 个不同的 \(a_x\)\(a_{x + 1}\)
但是,如果 \(a_x\)\(a_{x + 1}\) 都不是 y,那么根本没有影响,所以答案还是原来的;
而且,如果 \(a_x = a_{x + 1}\) 的话,那么还是没有影响;
接下来,进行分讨:

  • 假设 \(a_x = x\)\(a_{x + 1} = y\)。此时,我们考虑交换之后会有什么影响。很明显,现在的序列是 yx,那么,原来的 \(x + 1\) 的后面的所有的 z 都会减少一个可与匹配的 x,所以 \(sum\) 应该减掉 \(suf_{x + 1}\)

  • 假设 \(a_x = z\)\(a_{x + 1} = y\)。类比着上面的 ,我们很容易发现,\(sum\) 应该加上 \(pre_{x + 1}\)

  • 假设 \(a_x = y\)\(a_{x + 1} = x\)。类比着上面的 ,我们很容易发现,\(sum\) 应该加上 \(suf_x\)

  • 假设 \(a_x = y\)\(a_{x + 1} = z\)。类比着上面的 ,我们很容易发现,\(sum\) 应该减掉 \(pre_x\)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int M = 1e6 + 10;
char a[M];
ll n, suf[M][2], pre[M][2], ans, sum, q;

int main() {
	
	// freopen("decipher.in", "r", stdin);
	// freopen("decipher.out", "w", stdout);
	
	cin >> n >> q;
	for (int i = 1; i <= n; i ++) cin >> a[i];
	for (int i = 1; i <= n; i ++) {
		pre[i][0] = pre[i - 1][0], pre[i][1] = pre[i - 1][1];
		if (a[i] == 'x') pre[i][0] ++;
		if (a[i] == 'z') pre[i][1] ++;
	}
	for (int i = n; i >= 1; i --) {
		suf[i][0] = suf[i + 1][0], suf[i][1] = suf[i + 1][1];
		if (a[i] == 'x') suf[i][0] ++;
		if (a[i] == 'z') suf[i][1] ++;
	}
	for (int i = 1; i <= n; i ++) {
		if (a[i] == 'y') 
			sum += pre[i][0] * suf[i][1];
	}
	
//	cout << "sum = " << sum << "\n";
	
	while (q --) {
		int x; cin >> x;
		if (a[x] == 'y' && a[x + 1] == 'x') {
			sum += suf[x][1];
			pre[x][0] ++;
		}
		if (a[x] == 'y' && a[x + 1] == 'z') {
			sum -= pre[x][0];
			suf[x][1] --;
		}
		if (a[x] == 'x' && a[x + 1] == 'y') {
			sum -= suf[x + 1][1];
			pre[x + 1][0] --;
		}
		if (a[x] == 'z' && a[x + 1] == 'y') {
			sum += pre[x + 1][0];
			suf[x + 1][1] ++;
		}
		ans ^= sum;
		swap(a[x], a[x + 1]);
		swap(pre[x][0], pre[x + 1][0]);
		swap(pre[x][1], pre[x + 1][1]);
		swap(suf[x][0], suf[x + 1][0]);
		swap(suf[x][1], suf[x + 1][1]);
	}
	cout << ans << "\n";
	
	return 0;
}

文本难度:CSP-J T2
思维难度:CSP-J T3-T3.5/CSP-S T1.5-T2
代码实现难度:CSP-J T3/CSP-S T1.5

当然,本道题也可以使用线段树进行维护,而且可以对于任意 \(i\)\(j\) 的交换。
线段树做法具体的做法:

首先,我们考虑什么情况下会出现 \(xyz\)。很明显(可能不那么明显),要是 \(xy\)\(z\) 拼起来即可,也可以 \(yz\)\(x\) 拼起来即可。注意,这里的 \(xy\)\(yz\) 均指连续的字母。
考虑使用线段树。
我们维护一下 \([l, r]\) 之间的 \(xy\)\(yz\) 的个数,\(ans\) 表示区间的长度。我们考虑 \(ans_{pos}\) 可以通过什么来得到。
我们考虑左儿子和右儿子的 \(ans\)。很明显,\(ans_{pos} = ans_{pos \times 2} + ans_{pos \times 2 + 1}\)。同时,我们考虑左边和右边都没有统计到的个数。很明显,当且仅当左边的 \(xy\) 的个数乘以右边的 \(z\) 的个数还有左边的 \(x\) 的个数乘以右边的 \(yz\) 的个数。然后我们统计就可以了。这里给出 lzy 学长的代码。

//为什么要攀登?因为山就在那里。
#include<bits/stdc++.h>
#define mrx 0x7f7f7f7f7f7f7f7f
#define int long long
using namespace std;
inline int read(){
    int num=0,flag=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){
        if(ch=='-') flag=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        num=(num<<3)+(num<<1)+(ch^48);
        ch=getchar();
    }
    return num*flag;
}
inline void write(int num){
    if(num<0) putchar('-'),num=-num;
    if(num>9) write(num/10);
    putchar(num%10+'0');
}
inline void print(int num){
    write(num);
    putchar('\n');
}
inline void out(int num){
    write(num);
    putchar(' ');
}
inline int ksm(int a,int b,int mod){
	int ans=1;
	while(b){
		if(b&1) ans=ans*a%mod;
		a=a*a%mod,b>>=1;
	}
	return ans;
}
int n,_;
int ans;
string s;
struct line_tree{
	int x,y,z;
	int xy,yz;
	int ans;
}tree[1000010<<2];
void pushup(int id){
	tree[id].ans=tree[id<<1].ans+tree[id<<1|1].ans+(tree[id<<1].xy*tree[id<<1|1].z)+(tree[id<<1].x*tree[id<<1|1].yz); 
	tree[id].xy=tree[id<<1].xy+tree[id<<1|1].xy+(tree[id<<1].x*tree[id<<1|1].y);
	tree[id].yz=tree[id<<1].yz+tree[id<<1|1].yz+(tree[id<<1].y*tree[id<<1|1].z); 
	tree[id].x=tree[id<<1].x+tree[id<<1|1].x;
	tree[id].y=tree[id<<1].y+tree[id<<1|1].y;
	tree[id].z=tree[id<<1].z+tree[id<<1|1].z;
}
void make_tree(int L,int R,int id){
	if(L==R){
		if(s[L]=='x') tree[id].x++;
		if(s[L]=='y') tree[id].y++;
		if(s[L]=='z') tree[id].z++;
		return ;
	}
	int mid=(L+R)>>1;
	make_tree(L,mid,id<<1);
	make_tree(mid+1,R,id<<1|1);
	pushup(id);
}
void change1(int L,int R,int l,int id){
	if(L==R){
		if(s[l]=='x') tree[id].x--;
		if(s[l]=='y') tree[id].y--;
		if(s[l]=='z') tree[id].z--;
		if(s[l+1]=='x') tree[id].x++;
		if(s[l+1]=='y') tree[id].y++;
		if(s[l+1]=='z') tree[id].z++;
		return ;
	}
	int mid=(L+R)>>1;
	if(l<=mid) change1(L,mid,l,id<<1);
	else change1(mid+1,R,l,id<<1|1);
	pushup(id);
}
void change2(int L,int R,int l,int id){
	if(L==R){
		if(s[l]=='x') tree[id].x--;
		if(s[l]=='y') tree[id].y--;
		if(s[l]=='z') tree[id].z--;
		if(s[l-1]=='x') tree[id].x++;
		if(s[l-1]=='y') tree[id].y++;
		if(s[l-1]=='z') tree[id].z++;
		return ;
	}
	int mid=(L+R)>>1;
	if(l<=mid) change2(L,mid,l,id<<1);
	else change2(mid+1,R,l,id<<1|1);
	pushup(id);
}
signed main(){
	n=read(),_=read();
	cin>>s;s=' '+s;
	make_tree(1,n,1);
	while(_--){
		int x=read();
		change1(1,n,x,1);
		change2(1,n,x+1,1);
		swap(s[x],s[x+1]);
		ans^=tree[1].ans;
	}
	write(ans);
    return 0;
}
/*

*/

线段树做法:
文本难度:CSP-J T3/ CSP-S T1.5
思维难度:CSP-S T2
代码实现难度:CSP-J T4/CSP-S T2

T4

先说一句题外话,本题改编自P11969,是笔者在 luogu 月赛场切的一道题目,同时也是笔者在 luogu 上通过的第一篇题解。

我们先观察性质。
首先,先手一定不会选择合并操作,假如你看出了这个性质你就超过了 jmx 学长,同理,后手一定不会选择分裂操作。
同时,通过阅读题目,我们知道这里的字典序不是传统意义上的字典序,这很有利于简化题目难度。
接下来,对于 \(T\) 的奇偶性。

  • 假设 \(T\) 是奇数,也就是说先手最后一次操作。我们想一下,因为操作 \(1\) 和 操作 \(2\) 是可以来回转换的,所以先手先进行了一次最优操作,后手再干回去,以此类推。那么,我们分别考虑先手和后手的操作最优性。
    先手的希望是把字典序尽可能变小,也就是说让 \(1\) 尽可能在前面。形式化的,我们找到第一个 \(i\),使得 \(a_i > 1\),然后把 \(i\) 拆分成 \(1\)\(a_i - 1\) 即可。
    接下来考虑后手。很明显,后手的最优策略就是把 \(a_1\) 替换成 \(a_1 + a_2\)

  • 假设 \(T\) 是偶数。也就是说,是对方最后一次操作。
    假设后手最后一次操作,先手肯定把序列变成了像上面这个情况,接下来考虑后手。和上面的一样,后手的最优策略肯定是把 \(a_1\) 变成 \(a_1 + a_2\)
    假设先手最后一次操作,先手肯定是把第 \(1\) 个数字拆成了 \(1\)\(a_i - 1\)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int M = 1e5 + 10;
ll n, a[M], T;

void work1() {
	bool flag = 0, flag2 = 0;
	for (int i = 1; i <= n; i ++) {
		if (a[i] != 1) {
			flag = 1;
			break ;
		}
	}
	cout << n + flag << "\n";
	for (int i = 1; i <= n; i ++) {
		if (a[i] != 1 && flag2 == 0) {
			cout << 1 << " " << a[i] - 1 << " ";
			flag2 = 1;
		} else {
			cout << a[i] << " ";
		}
	}
	cout << "\n";
	if (n == 1) {
		cout << a[1] << "\n";
		return ;
	}
	cout << n - 1 << "\n";
	for (int i = 2; i <= n; i ++) {
		if (i == 2) cout << a[1] + a[2] << " ";
		else cout << a[i] << " ";
	} 
	cout << "\n";
}

void work2() {
	bool flag2 = 0;
	int now = 0;
	vector < int > b(n + 2);
	for (int i = 1; i <= n; i ++) {
		if (a[i] != 1 && flag2 == 0) {
			b[++ now] = 1; b[++ now] = a[i] - 1;
			flag2 = 1;
		} else {
			b[++ now] = a[i];
		}
	}
	if (now == 1) {
		cout << now << "\n";
		cout << b[1] << "\n";
	} else {
		cout << now - 1 << "\n";
		for (int i = 2; i <= now; i ++) {
			if (i == 2) cout << b[1] + b[2] << " ";
			else cout << b[i] << " ";
		} 
		cout << "\n";
	}
	if (n == 1) {
		if (a[1] == 1) {
			cout << n << "\n";
			cout << "1\n";
			return ; 
		} else {
			cout << 2 << "\n";
			cout << 1 << " " << a[1] - 1 << "\n";
		}
	} else {
		cout << n << "\n";
		int t = a[1] + a[2];
		a[2] = t - 1;
		cout << 1 << " ";
		for (int i = 2; i <= n; i ++) 
			cout << a[i] << " ";
		cout << "\n";
	}
}

int main() {
	
//	freopen("emperor.in", "r", stdin);
//	freopen("emperor.out", "w", stdout);
	
	cin >> T >> n;
	for (int i = 1; i <= n; i ++) 
		cin >> a[i];
	if (T % 2 == 1) work1();
	else work2();
	
	return 0;
}

文本难度:CSP-J T3.5-T4/CSP-S T2-T2.5
思维难度:CSP-J T3.5-T4/CSP-S T2-T2.5
代码难度:CSP-J T4/CSP-S T2.5

posted @ 2025-10-26 17:29  Bob1108  阅读(19)  评论(0)    收藏  举报