学弟欢乐赛 - 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
浙公网安备 33010602011771号