把连续可交换位置压成区间容量,分别贪心匹配同为 1 和同为 0 的最大数量。
OJ: luogu
题目 ID: P11361
难度:普及+/提高
标签:字符串贪心区间
日期: 2026-06-22 19:01
题意
有两个长度为 n 的二进制字符串 s1, s2。每个字符串还有一个限制串 t:t[i]=1 表示这个位置的字符可以参与相邻交换,t[i]=0 表示不能参与交换。
可以分别对两个字符串做任意多次合法相邻交换,问最终最多有多少个位置满足两个字符串字符相同。
思路
先看一个小数据暴力:枚举每个可交换连续段的所有最终排列,再枚举两个字符串的最终形态并比较。
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
int T, n;
string s1, s2, t1, t2;
vector<string> final_a, final_b;
void build_finals_dfs(const string &s, const string &t, int pos, string current, vector<string> &result) {
if (pos == n) {
result.push_back(current);
return;
}
if (t[pos] == '0') {
build_finals_dfs(s, t, pos + 1, current + s[pos], result);
return;
}
int end_pos = pos;
int count_one = 0;
while (end_pos < n && t[end_pos] == '1') {
count_one += s[end_pos] == '1';
end_pos++;
}
int len = end_pos - pos;
int total = 1 << len;
for (int mask = 0; mask < total; mask++) {
int bits = 0;
for (int i = 0; i < len; i++) {
if (mask & (1 << i)) {
bits++;
}
}
if (bits != count_one) {
continue;
}
string next_string = current;
for (int i = 0; i < len; i++) {
if (mask & (1 << i)) {
next_string.push_back('1');
} else {
next_string.push_back('0');
}
}
build_finals_dfs(s, t, end_pos, next_string, result);
}
}
int calc_score(const string &a, const string &b) {
int score = 0;
for (int i = 0; i < n; i++) {
if (a[i] == b[i]) {
score++;
}
}
return score;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
cin >> n;
cin >> s1 >> s2 >> t1 >> t2;
final_a.clear();
final_b.clear();
build_finals_dfs(s1, t1, 0, "", final_a);
build_finals_dfs(s2, t2, 0, "", final_b);
int ans = 0;
for (int i = 0; i < (int)final_a.size(); i++) {
for (int j = 0; j < (int)final_b.size(); j++) {
ans = max(ans, calc_score(final_a[i], final_b[j]));
}
}
cout << ans << '\n';
}
return 0;
}暴力的瓶颈在于,一个长的可交换段会有很多种排列。
关键观察是:一个连续可交换段内,靠相邻交换可以得到任意排列。对于二进制串来说,这一段最终只需要保留两个信息:
区间 [l,r],以及其中 1 的数量 cnt不能交换的位置就看作长度为 1 的段。
先只考虑“两个字符串同为 1”的位置数。对 s1 和 s2 分别拆段后,每段都是一个区间容量 [l,r,cnt],表示这个区间里可以安排 cnt 个 1。
然后用双指针从左到右匹配两个段:
- 当前两段的重叠部分长度为
len; - 能同时放
1的数量为min(len, rest1, rest2); - 匹配后扣掉两边剩余的
1; - 哪一段右端点更靠左,就移动哪一侧指针。
为什么可以贪心?因为当前重叠区间一旦错过,之后至少有一侧指针右移,这段位置不会再回来。能在这里匹配的目标字符尽量匹配,不会让后面的机会变差。
同为 0 的数量也可以用同一个方法求:把两个字符串的 0/1 取反,原来的 0 就变成目标字符 1。
所以答案是:
最大同为 1 的数量 + 最大同为 0 的数量代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
struct Block {
int left, right;
int count_one;
};
int T, n;
string s1, s2, t1, t2;
Block block_a[MAXN], block_b[MAXN];
int cnt_a, cnt_b;
void build_blocks(const string &s, const string &t, Block block[], int &cnt, bool flip_bit) {
cnt = 0;
int i = 0;
while (i < n) {
if (t[i] == '0') {
int value = s[i] - '0';
if (flip_bit) {
value ^= 1;
}
cnt++;
block[cnt].left = i + 1;
block[cnt].right = i + 1;
block[cnt].count_one = value;
i++;
} else {
int j = i;
int count_one = 0;
while (j < n && t[j] == '1') {
int value = s[j] - '0';
if (flip_bit) {
value ^= 1;
}
count_one += value;
j++;
}
cnt++;
block[cnt].left = i + 1;
block[cnt].right = j;
block[cnt].count_one = count_one;
i = j;
}
}
}
int count_same_one(bool flip_bit) {
build_blocks(s1, t1, block_a, cnt_a, flip_bit);
build_blocks(s2, t2, block_b, cnt_b, flip_bit);
int i = 1, j = 1;
int rest_a = block_a[1].count_one;
int rest_b = block_b[1].count_one;
int ans = 0;
while (i <= cnt_a && j <= cnt_b) {
int left = max(block_a[i].left, block_b[j].left);
int right = min(block_a[i].right, block_b[j].right);
if (left <= right) {
int len = right - left + 1;
int take = min(len, min(rest_a, rest_b));
ans += take;
rest_a -= take;
rest_b -= take;
}
if (block_a[i].right < block_b[j].right) {
i++;
if (i <= cnt_a) {
rest_a = block_a[i].count_one;
}
} else if (block_a[i].right > block_b[j].right) {
j++;
if (j <= cnt_b) {
rest_b = block_b[j].count_one;
}
} else {
i++;
j++;
if (i <= cnt_a) {
rest_a = block_a[i].count_one;
}
if (j <= cnt_b) {
rest_b = block_b[j].count_one;
}
}
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T;
while (T--) {
cin >> n;
cin >> s1 >> s2 >> t1 >> t2;
int same_one = count_same_one(false);
int same_zero = count_same_one(true);
cout << same_one + same_zero << '\n';
}
return 0;
}复杂度
每次拆段和匹配都是 1 和同为 0 各做一次。
总时间复杂度为
总结
本题的关键是不要真的模拟相邻交换。连续可交换段可以任意重排,所以只需要把它压成“区间 + 目标字符数量”。
之后问题变成两个区间容量序列的最大重叠匹配,用双指针贪心即可。统计 1 做一次,取反后统计 0 再做一次,二者相加就是最大相同位置数。