1 条题解

  • 0
    @ 2025-11-18 19:43:47

    题目分析

    题意理解

    我们有一棵树结构,每个节点 i 的父节点是 ⌊i/k⌋。需要给每个节点分配一个难度值,满足:

    1. 树约束:每个节点的难度 ≥ 其父节点的难度
    2. 字典序最大:在满足条件1的情况下,使得序列的字典序最大

    关键观察

    1. 树结构:这实际上是一个完全k叉树的结构
    2. 贪心策略:为了得到字典序最大的解,我们应该从前往后为每个位置分配尽可能大的值
    3. 预留空间:给某个节点分配值时,需要为其子树预留足够的位置

    算法思路

    核心思想:线段树 + 贪心

    1. 预处理

    • 排序难度数组
    • 计算每个节点的子树大小
    • 处理相同难度的计数(用于去重)

    2. 线段树维护

    线段树 tr[u] 维护:从位置 i 开始往右,还能选择多少个值(考虑子树大小约束)

    初始时:tr[i] = n - i + 1,表示位置 i 及右边总共有多少个空位

    3. 贪心过程

    对于每个位置 i(从1到n):

    1. 如果父节点还没有处理,先处理父节点的约束
    2. 在线段树上二分查找最左边的满足 tr[x] >= sz[i] 的位置
    3. 选择该位置对应的难度值
    4. 更新线段树:为节点 i 的子树预留空间

    代码详解

    #include <bits/stdc++.h>
    using namespace std;
    
    #define ll long long
    #define pb push_back
    #define ls u*2
    #define rs u*2+1
    #define mid (l+r)/2
    
    const int N = 5e5 + 5;
    
    int n, d[N], fa[N], sz[N], cnt[N], ans[N];
    double k;
    bool vis[N];
    
    // 线段树维护可用位置
    int tr[4 * N], lz[4 * N];
    
    // 更新节点
    void upd(int u) {
        tr[u] = min(tr[ls], tr[rs]);
    }
    
    // 懒标记下传
    void tag(int u, int k) {
        tr[u] += k, lz[u] += k;
    }
    
    void pd(int u) {
        if (lz[u]) {
            tag(ls, lz[u]), tag(rs, lz[u]);
            lz[u] = 0;
        }
    }
    
    // 建树:初始每个位置i的可用数量为n-i+1
    void build(int l, int r, int u) {
        if (l == r) {
            tr[u] = n - l + 1;
            return;
        }
        build(l, mid, ls), build(mid + 1, r, rs);
        upd(u);
    }
    
    // 区间加
    void add(int l, int r, int u, int L, int R, int k) {
        if (l >= L && r <= R) {
            tag(u, k);
            return;
        }
        pd(u);
        if (L <= mid) add(l, mid, ls, L, R, k);
        if (R > mid) add(mid + 1, r, rs, L, R, k);
        upd(u);
    }
    
    // 二分查找:找到第一个tr[x] < k的位置
    int find(int l, int r, int u, int k) {
        if (tr[u] >= k) return -1;  // 没有满足的位置
        
        if (l == r) return l;  // 找到目标位置
        
        pd(u);
        
        // 优先在左子树查找
        if (tr[ls] < k) 
            return find(l, mid, ls, k);
        else 
            return find(mid + 1, r, rs, k);
    }
    
    int main() {
        scanf("%d%lf", &n, &k);
        
        // 读入并排序难度值
        for (int i = 1; i <= n; i++)
            scanf("%d", &d[i]);
        sort(d + 1, d + n + 1);
        
        // 预处理相同值的计数
        for (int i = 2; i <= n; i++)
            if (d[i] == d[i - 1])
                cnt[i] = cnt[i - 1] + 1;
        
        // 构建树结构
        for (int i = 1; i <= n; i++)
            fa[i] = i / k;  // 父节点编号
        
        // 计算每个节点的子树大小(自底向上)
        for (int i = n; i >= 1; i--) {
            sz[i]++;
            sz[fa[i]] += sz[i];
        }
        
        // 初始化线段树
        build(1, n, 1);
        
        // 贪心分配难度值
        for (int i = 1; i <= n; i++) {
            // 处理父节点的约束:为父节点的子树释放空间
            if (fa[i] && !vis[fa[i]]) {
                vis[fa[i]] = 1;
                // 父节点已经确定,释放多余的预留空间
                add(1, n, 1, 1, ans[fa[i]], sz[fa[i]] - 1);
            }
            
            // 二分查找最左边的满足条件的位置
            int x = find(1, n, 1, sz[i]);
            
            if (x == -1) 
                x = n;      // 如果没有找到,用最后一个位置
            else 
                x--;        // 找到的是第一个不满足的位置,前一个位置满足
            
            // 处理相同值:选择最右边的相同值
            x -= cnt[x];
            ans[i] = x + cnt[x];
            cnt[x]++;
            
            // 为当前节点的子树预留空间
            add(1, n, 1, 1, ans[i], -sz[i]);
        }
        
        // 输出结果
        for (int i = 1; i <= n; i++)
            printf("%d ", d[ans[i]]);
        
        return 0;
    }
    

    算法核心要点详解

    1. 线段树的作用

    线段树 tr[i] 表示:从位置 i 开始往右,考虑所有已分配的子树约束后,还能选择多少个值。

    关键理解

    • 初始:tr[i] = n - i + 1(位置i右边总共有这么多空位)
    • 当为节点分配位置 x 时,需要为其大小为 sz 的子树预留空间:
      • 对区间 [1, x] 执行 -sz 操作
      • 表示这些位置左边多了一个大小为 sz 的子树

    2. 二分查找的逻辑

    find(1, n, 1, sz[i]) 寻找第一个 tr[x] < sz[i] 的位置 x。

    为什么这样找

    • 我们要找最左边的满足 tr[pos] >= sz[i] 的位置
    • 即:从该位置开始往右,至少有 sz[i] 个可用位置
    • 这样能保证为子树预留足够空间

    3. 相同值的处理

    x -= cnt[x];
    ans[i] = x + cnt[x];
    cnt[x]++;
    

    这段代码确保相同难度值时,我们选择最右边的相同值,这是为了字典序最大。

    4. 父节点处理

    当处理到节点 i 时,如果其父节点还没有被标记处理:

    • 释放父节点多余的预留空间
    • 因为父节点已经确定,其子树大小约束可以放宽

    举例说明

    以样例为例:

    输入: n=4, k=2.0, d=[114,514,1919,810]
    排序后: d=[114,514,810,1919]
    树结构: 
      1(父:0)
      2(父:1) 
      3(父:1)
      4(父:2)
    

    分配过程:

    1. 节点1:选择最右边的满足条件的位置 → 114
    2. 节点2:在剩余位置中选择 → 810
    3. 节点3:选择 → 514
    4. 节点4:选择 → 1919

    结果:114 810 514 1919

    复杂度分析

    • 排序:O(n log n)
    • 线段树操作:每次 O(log n)
    • 总复杂度:O(n log n)

    总结

    这道题的巧妙之处在于:

    1. 逆向思维:用线段树维护"剩余可用位置"而非直接分配
    2. 贪心策略:在满足子树约束的前提下尽可能选择大的值
    3. 相同值处理:通过计数确保字典序最大
    4. 树形约束:通过子树大小来预留空间

    是一个很好的数据结构与贪心思想结合的题目。

    • 1

    信息

    ID
    5093
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者