讨厌鬼的数组构造

[题目链接](https://www.nowcoder.com/practice/883192b10c7a4527a469abe5967c7905)

思路

给定长度为 的序列 ,要求构造一个长度为 的序列 ,满足:

  1. 开始编号)
  2. 所有 互不相同

确定候选值

对于位置 ,条件 等价于

,若 则令 。则 的候选值为 。由于 ,当 时从 开始取。

贪心策略

关键观察:编号越大的位置,步长越大,可选的值越稀疏。因此应该优先给大编号的位置分配值,再给小编号的位置分配——小编号位置步长小,候选值密集,容易找到未被占用的值。

具体做法: 倒序处理。对每个位置 ,从最小候选值开始,找到第一个未被使用的值,分配给 并标记已用。

为什么一定能构造成功?

对于位置 ,候选值的间隔为 ,在 范围内至少有 个候选值。而最多只有 个值已被占用,所以一定能找到可用的值。实际上由于我们优先处理大步长位置,冲突非常少,总查找次数近似

样例演示

,从 开始:

候选序列 选中
5 10 ,从 5, 10, 15, ... 5
4 8 ,从 4, 8, 12, ... 4
3 7 2, 5, 8, ... 2
2 4 ,从 2, 4, 6, ... 6
1 3 ,从 1, 2, 3, ... 1

输出 ,验证每个位置 均成立。

代码

#include <bits/stdc++.h>
using namespace std;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n;
    cin >> n;
    vector<int> a(n+1);
    for(int i = 1; i <= n; i++) cin >> a[i];

    vector<long long> b(n+1, 0);
    set<long long> used;

    for(int i = n; i >= 1; i--){
        long long r = (long long)i - (a[i] % i);
        if(r == i) r = 0;
        long long start = (r == 0) ? i : r;
        for(long long v = start; ; v += i){
            if(used.find(v) == used.end()){
                b[i] = v;
                used.insert(v);
                break;
            }
        }
    }

    for(int i = 1; i <= n; i++){
        cout << b[i];
        if(i < n) cout << ' ';
    }
    cout << '\n';
    return 0;
}
import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine().trim());
        StringTokenizer st = new StringTokenizer(br.readLine().trim());
        int[] a = new int[n + 1];
        for (int i = 1; i <= n; i++) a[i] = Integer.parseInt(st.nextToken());

        long[] b = new long[n + 1];
        Set<Long> used = new HashSet<>();

        for (int i = n; i >= 1; i--) {
            long r = (long) i - (a[i] % i);
            if (r == i) r = 0;
            long start = (r == 0) ? i : r;
            for (long v = start; ; v += i) {
                if (!used.contains(v)) {
                    b[i] = v;
                    used.add(v);
                    break;
                }
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= n; i++) {
            if (i > 1) sb.append(' ');
            sb.append(b[i]);
        }
        System.out.println(sb);
    }
}
import sys
input = sys.stdin.readline

def main():
    n = int(input())
    a = [0] + list(map(int, input().split()))

    b = [0] * (n + 1)
    used = set()

    for i in range(n, 0, -1):
        r = i - (a[i] % i)
        if r == i:
            r = 0
        start = i if r == 0 else r
        v = start
        while v in used:
            v += i
        b[i] = v
        used.add(v)

    print(' '.join(str(b[i]) for i in range(1, n + 1)))

main()
const readline = require('readline');
const rl = readline.createInterface({ input: process.stdin });
const lines = [];
rl.on('line', line => lines.push(line.trim()));
rl.on('close', () => {
    const n = parseInt(lines[0]);
    const a = [0, ...lines[1].split(' ').map(Number)];

    const b = new Array(n + 1).fill(0);
    const used = new Set();

    for (let i = n; i >= 1; i--) {
        let r = i - (a[i] % i);
        if (r === i) r = 0;
        let start = r === 0 ? i : r;
        let v = start;
        while (used.has(v)) v += i;
        b[i] = v;
        used.add(v);
    }

    const res = [];
    for (let i = 1; i <= n; i++) res.push(b[i]);
    console.log(res.join(' '));
});

复杂度分析

  • 时间复杂度。每个位置平均只需尝试常数次候选值(冲突极少),每次 set 操作
  • 空间复杂度。存储数组 和已用值集合。