今天我们来探讨一个有趣的排列构造问题,这个问题来自某知名编程竞赛平台。题目要求我们根据给定的01字符串构造一个特定规则的排列,或者判断其不存在性。这类问题在实际编程面试和算法竞赛中经常出现,考察选手对排列性质的理解和构造能力。
首先明确几个关键概念:
排列:由1到n这n个整数组成的数组,每个整数恰好出现一次。例如n=3时,[1,2,3]、[3,1,2]都是合法的排列。
匹配条件:字符串s与排列p匹配需要满足:
注意:题目中的字符串索引从0开始,而排列长度从1开始计数,这个细节在实际编码时需要特别注意。
通过分析题目描述和示例,我们可以得出几个关键观察:
前缀排列性:当s[i]='1'时,意味着前i+1个元素必须恰好包含1到i+1的所有整数且不重复。这实际上要求前i+1个元素构成一个排列。
严格递增性:字符串中'1'的出现位置决定了排列必须满足的严格条件。例如,如果s="101",那么:
矛盾检测:当s的最后一个字符是'0'时,必然无解,因为整个排列本身必须是一个完整排列(这与s[n-1]='0'矛盾)。
基于上述观察,我们可以设计如下算法:
预处理检查:
构造策略:
具体实现:
我们选择以下数据结构来实现算法:
cpp复制#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
string s;
cin >> s;
vector<int> a; // 记录'0'的位置
vector<int> b; // 记录'1'的位置
stack<int> st; // 用于匹配0和1
// 第一步:匹配0和1的位置
for(int i = 0; i < n; i++) {
if(s[i] == '0' && st.empty()) {
st.push(0);
a.push_back(i);
}
if(s[i] == '1' && !st.empty()) {
b.push_back(i);
st.pop();
}
}
vector<int> res;
for(int i = 1; i <= n; i++) {
res.push_back(i);
}
// 处理无解情况
if(a.size() > b.size()) {
cout << -1;
}
// 无需交换的情况
else if(a.empty()) {
for(int i = 0; i < n; i++) {
cout << res[i] << ' ';
}
}
// 需要交换的情况
else {
int cnt = a.size();
for(int i = 0; i < cnt; i++) {
swap(res[a[i]], res[b[i]]);
}
for(int i = 0; i < n; i++) {
cout << res[i] << ' ';
}
}
return 0;
}
位置匹配:使用栈来匹配'0'和'1'的位置。遇到'0'入栈,遇到'1'出栈,确保每个'0'都能找到对应的'1'。
交换策略:在基础排列上,交换匹配的'0'和'1'位置的元素。这样做的目的是:
边界处理:
为了验证我们算法的正确性,我们需要证明:
充分性:构造的排列满足所有条件
必要性:如果问题无解,算法确实能返回-1
通过示例分析可以验证这一点:
示例1:s="001"
示例2:s="1110"
在实际实现中,容易遇到以下问题:
索引混淆:
边界条件处理不足:
交换逻辑错误:
调试技巧:对于n较小的情况,可以手动模拟算法过程,打印每一步的中间状态,确保逻辑正确。
这个问题可以有多种变体,值得进一步探索:
在实际工程中,类似的思路可以应用于:
在解决这个问题的过程中,我总结了以下几点经验:
问题分析比编码更重要:花足够时间理解题目条件和隐含约束,可以节省大量调试时间。
小规模测试先行:先用小例子手动验证思路,再推广到一般情况。
数据结构选择关键:栈结构在这个问题中起到了关键作用,恰当的数据结构能大大简化问题。
边界条件决定成败:算法竞赛中,很多错误都来自边界条件处理不当。
代码可读性很重要:即使是在竞赛中,清晰的代码结构和适当的注释也能帮助减少错误。
这个问题的核心在于理解排列的性质和字符串条件的对应关系。通过将抽象的条件转化为具体的元素操作,我们能够构造出满足要求的排列。这种"问题转化"的能力在算法设计中至关重要。