固定首个取数方向后按匹配位置切成两段,用两端配对贪心构造操作串。
OJ: luogu
题目 ID: P7915
难度:提高+/省选-
标签:构造贪心双指针搜索
日期: 2026-07-06 08:46
题意
给定长度为 2n 的序列,其中 1..n 每个数都恰好出现两次。每次可以从当前序列左端或右端取出一个数,依次放到新序列 b 的末尾。
要求构造一个操作串,使最终得到的 b 是回文;如果有多种方案,输出字典序最小的操作串。规定 L < R。
思路
小数据可以直接按字典序枚举所有操作串,找到第一个能得到回文的方案:
// brute.cpp:小数据暴力解,按字典序 DFS 枚举 L/R 操作直到得到回文。
#include <bits/stdc++.h>
using namespace std;
int n;
vector<int> origin;
string answer;
bool found;
bool is_palindrome(const vector<int> &b) {
for (int i = 0, j = (int)b.size() - 1; i < j; i++, j--) {
if (b[i] != b[j]) {
return false;
}
}
return true;
}
void dfs(int l, int r, vector<int> &b, string &ops) {
if (found) {
return;
}
if (l > r) {
if (is_palindrome(b)) {
answer = ops;
found = true;
}
return;
}
// 字典序要求优先尝试 L。
b.push_back(origin[l]);
ops.push_back('L');
dfs(l + 1, r, b, ops);
ops.pop_back();
b.pop_back();
b.push_back(origin[r]);
ops.push_back('R');
dfs(l, r - 1, b, ops);
ops.pop_back();
b.pop_back();
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
cin >> n;
origin.assign(2 * n + 1, 0);
for (int i = 1; i <= 2 * n; i++) {
cin >> origin[i];
}
answer.clear();
found = false;
vector<int> b;
string ops;
dfs(1, 2 * n, b, ops);
if (found) {
cout << answer << '\n';
} else {
cout << -1 << '\n';
}
}
return 0;
}暴力的瓶颈是操作串有 2^(2n) 种。正解需要利用“每个数恰好出现两次”的性质。
一个回文序列的第一个数和最后一个数必须相同。因此,第一步只有两种本质尝试:
- 先取左端
a[1],那么它的另一次出现必须作为最后一步被取走; - 先取右端
a[2n],那么它的另一次出现必须作为最后一步被取走。
因为 L 的字典序小于 R,我们先尝试第一种。如果能构造成功,它一定优于任何以 R 开头的方案。
以先取左端为例。设 a[1] 的另一次出现位置为 p。位置 p 的数要留到最后,剩下的元素被切成两段:
[2, p-1] [p+1, 2n]后续每次要为回文的前半部分取一个数,同时为回文的后半部分预定一个相同的数。当前可取的前端只可能是左段左端或右段右端;对应的后端只可能是左段右端或右段左端。
所以每一步最多检查四种配对:
左段左端 <-> 左段右端
左段左端 <-> 右段左端
右段右端 <-> 左段右端
右段右端 <-> 右段左端为了字典序最小,能让当前操作为 L 时优先选 L,否则再选 R。每次记录前半部分操作,同时把匹配操作记录到后半部分;最后后半部分要反向拼回去。
如果先取左端失败,再用同样方法尝试先取右端。两次都失败才输出 -1。
代码
// main.cpp:固定第一个选择后,把剩余序列分成两段,贪心构造字典序最小方案。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
int n;
int a[MAXN];
int first_pos[MAXN], second_pos[MAXN];
bool same_pos_invalid(int x, int y) {
return x == y;
}
bool build_answer(bool choose_left_first, string &answer) {
int total = 2 * n;
int first_value;
int match_pos;
char first_op, last_op;
int l1, r1, l2, r2;
if (choose_left_first) {
first_value = a[1];
match_pos = (first_pos[first_value] == 1) ? second_pos[first_value] : first_pos[first_value];
first_op = 'L';
last_op = 'L';
l1 = 2;
r1 = match_pos - 1;
l2 = match_pos + 1;
r2 = total;
} else {
first_value = a[total];
match_pos = (first_pos[first_value] == total) ? second_pos[first_value] : first_pos[first_value];
first_op = 'R';
// 最后只剩一个元素时,L/R 都能取走;为了字典序取 L。
last_op = 'L';
l1 = 1;
r1 = match_pos - 1;
l2 = match_pos + 1;
r2 = total - 1;
}
string left_ops, right_ops;
left_ops.push_back(first_op);
for (int step = 1; step <= n - 1; step++) {
bool done = false;
if (l1 <= r1) {
if (l1 < r1 && a[l1] == a[r1]) {
left_ops.push_back('L');
right_ops.push_back('L');
l1++;
r1--;
done = true;
} else if (l2 <= r2 && a[l1] == a[l2]) {
left_ops.push_back('L');
right_ops.push_back('R');
l1++;
l2++;
done = true;
}
}
if (!done && l2 <= r2) {
if (l1 <= r1 && a[r2] == a[r1]) {
left_ops.push_back('R');
right_ops.push_back('L');
r2--;
r1--;
done = true;
} else if (l2 < r2 && a[r2] == a[l2]) {
left_ops.push_back('R');
right_ops.push_back('R');
r2--;
l2++;
done = true;
}
}
if (!done) {
return false;
}
}
if (l1 <= r1 || l2 <= r2) {
return false;
}
answer = left_ops;
for (int i = (int)right_ops.size() - 1; i >= 0; i--) {
answer.push_back(right_ops[i]);
}
answer.push_back(last_op);
return true;
}
void solve_one() {
cin >> n;
int total = 2 * n;
for (int i = 1; i <= n; i++) {
first_pos[i] = second_pos[i] = 0;
}
for (int i = 1; i <= total; i++) {
cin >> a[i];
if (first_pos[a[i]] == 0) {
first_pos[a[i]] = i;
} else {
second_pos[a[i]] = i;
}
}
string answer;
if (build_answer(true, answer)) {
cout << answer << '\n';
return;
}
if (build_answer(false, answer)) {
cout << answer << '\n';
return;
}
cout << -1 << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) {
solve_one();
}
return 0;
}复杂度
每组数据只会在线性指针上移动,每个位置最多被处理一次。
时间复杂度为
总结
本题不要直接搜索完整操作串。先固定第一个数,就能确定最后一个数的位置;这个位置把剩余序列切成两段,后面的构造就变成“两段两端配对”。
字典序最小的核心是:先尝试首字母为 L 的方案,构造过程中也尽量让前半部分当前操作为 L。