题目链接

小红的排列构造②

题目描述

小红定义一个仅由 '0' 和 '1' 两个字符构成的字符串 与一个长度为 的数组 为匹配,当且仅当满足下列两点:

  • ,则数组 的前 项(即 )恰好构成一个 的排列。
  • ,则数组 的前 项(即 )无法构成一个 的排列。

现在小红给出了一个长度为 的字符串 ,请你构造一个长度为 的排列 使得 匹配;如果不存在这样的排列,请输出 -1。

解题思路

这是一个构造性问题。我们需要根据字符串 的要求,一步步构建出排列

首先,我们分析题目给出的两个核心条件:

  • s[i] = '1':这意味着集合 必须等于集合
  • s[i] = '0':这意味着集合 不能等于集合

一个至关重要的约束来自排列 本身的定义: 必须是 的一个排列。这意味着,当 时,前 项(即整个数组)必须包含且仅包含 的所有整数。这恰好满足了 的条件。因此,如果给定的字符串 最后一个字符 是 '0',那么构造一个满足条件的排列 是不可能的,因为这与 必须是 的排列相矛盾。所以,这是第一个无解的情况。

接下来,我们考虑如何构造。一个有效的策略是从左到右遍历字符串 ,并把问题分解成若干个独立的“块”。每当遇到一个 '1',就意味着一个前缀的构成被确定下来,这可以视为一个块的结束。

具体来说,一个块由若干个(可能为零个)'0' 跟着一个 '1' 组成。例如,如果 ,我们可以将其分解为三个块:

  1. s[0...2] = "001"
  2. s[3...4] = "01"
  3. s[5...5] = "1"

对于第一个块 s[0...2],它结束于 的条件要求 必须是 。同时,块内的 '0' 提供了额外的约束:

如何满足这些 '0' 的约束呢?一个简单的方法是把块内可用的最大数字放在最前面。对于块 s[0...2],可用的数字是 ,对应的位置是 。如果我们倒序赋值:

  • 检查 :前缀为 ,不是 ,满足。
  • 检查 :前缀为 ,不是 ,满足。
  • 检查 :前缀为 ,是 的排列,满足。

这个策略是通用的。对于一个结束于 ,开始于 的块(即 都是 '0'),我们需要将数字 填入位置 。通过将这些数字降序填入,即 ,我们可以保证所有中间的 '0' 条件都得到满足,因为每个前缀都会提前包含一个比当前前缀范围要求大的数。

基于此,我们可以设计出如下算法:

  1. 检查 是否为 '0',如果是,则输出 -1。
  2. 维护一个待分配数字的列表 pending_numbers
  3. 遍历 : a. 将数字 加入 pending_numbers。 b. 如果 ,说明一个块结束了。此时 pending_numbers 里的所有数字需要被分配。我们将 pending_numbers 中的数字逆序(从大到小)依次填入当前块对应的位置中。

代码

#include <iostream>
#include <vector>
#include <string>
#include <numeric>

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    int n;
    cin >> n;
    string s;
    cin >> s;

    if (s.back() == '0') {
        cout << -1 << endl;
        return 0;
    }

    vector<int> p(n);
    vector<int> pending_numbers;
    int current_block_start = 0;

    for (int i = 0; i < n; ++i) {
        pending_numbers.push_back(i + 1);
        if (s[i] == '1') {
            int current_pos = i;
            while (!pending_numbers.empty()) {
                p[current_pos--] = pending_numbers.front();
                pending_numbers.erase(pending_numbers.begin());
            }
        }
    }

    for (int i = 0; i < n; ++i) {
        cout << p[i] << (i == n - 1 ? "" : " ");
    }
    cout << endl;

    return 0;
}
import java.util.Scanner;
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        String s = sc.next();

        if (s.charAt(n - 1) == '0') {
            System.out.println(-1);
            return;
        }

        int[] p = new int[n];
        ArrayList<Integer> pendingNumbers = new ArrayList<>();
        
        for (int i = 0; i < n; i++) {
            pendingNumbers.add(i + 1);
            if (s.charAt(i) == '1') {
                int currentPos = i;
                while (!pendingNumbers.isEmpty()) {
                    p[currentPos--] = pendingNumbers.remove(0);
                }
            }
        }

        for (int i = 0; i < n; i++) {
            System.out.print(p[i] + (i == n - 1 ? "" : " "));
        }
        System.out.println();
    }
}
def solve():
    n = int(input())
    s = input()

    if s[-1] == '0':
        print(-1)
        return

    p = [0] * n
    pending_numbers = []
    
    for i in range(n):
        pending_numbers.append(i + 1)
        if s[i] == '1':
            start_pos = i - len(pending_numbers) + 1
            # 将 pending_numbers 逆序填入 p[start_pos ... i]
            p[start_pos : i + 1] = pending_numbers[::-1]
            pending_numbers = []
            
    print(*p)

solve()

算法及复杂度

  • 算法:贪心、构造
  • 时间复杂度:。虽然代码中对于 pending_numbers 的操作(如 vector::eraseArrayList::remove(0))在循环中可能看起来是平方级的,但每个数字 只会被加入和移出列表一次。因此,总的时间复杂度是线性的。Python版本使用切片赋值,整体也是线性的。
  • 空间复杂度:,用于存储结果数组 和辅助列表 pending_numbers