#P2908. 第2题-数组重新排序
          
                        
                                    
                      
        
              - 
          
          
                      1000ms
            
          
                      Tried: 5
            Accepted: 4
            Difficulty: 5
            
          
          
          
                       所属公司 : 
                              阿里
                                
            
                        
              时间 :2025年4月24日-阿里淘天(开发岗)
                              
                      
          
 
- 
                        算法标签>思维          
 
第2题-数组重新排序
题解
题面描述
给定一个长度为 n 的数组 {a1,a2,…,an}。可以进行若干次如下操作:
i=l∑rai.
- 任选一个区间 [l,r],将子数组 {al,al+1,…,ar} 的所有元素任意重排;
 - 本次操作代价为该区间元素之和:
 
要求计算:
- 将数组变为升序所需的最小总代价;
 - 将数组变为降序所需的最小总代价。
 
n 的取值可达 2×105,ai 可达 109,需要 O(nlogn) 或更优方案。
思路
- 
全局思路
- 任何一次操作只能在某个连续区间内任意置换,区间外的元素位置保持不动;
 - 如果对多个不相交区间各做一次操作,且每个元素至多参与一次操作,则总代价等于这些区间内所有元素之和;
 - 因此,要最小化总代价,就应尽可能地保留一些“本来就在正确位置”的元素,不对它们所在的区间做操作。
 
 - 
如何判断元素可“保留”
- 令 b 为数组 a 排序(升序或降序)后得到的目标数组;
 - 若在下标 i 上有 ai=bi,且前缀 a[1..i] 与 b[1..i] 的多重集合相同,则可以将位置 i 视为一个“分割点”,
在该点可将操作区间切开,不必包含这个位置; - 记 P[i]=true 当且仅当多重集合{a1,…,ai}={b1,…,bi}.
 - 那么所有满足P[i]且ai=bi 的位置都可以“保留”不动。
 - 总和 S=∑i=1nai,保留元素之和记为
Skeep=sumi:P[i]∧ai=biai. - 最小总代价即
S−Skeep. 
 - 
复杂度
- 先拷贝一份数组并排序得到 b,O(nlogn);
 - 一次扫描维护两个哈希表(或 
unordered_map):前缀中对每个值的出现次数差异,判断 P[i],共 O(n) 平均; - 总计 O(nlogn)。
 
 
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 计算将 a 排成 b 需要的最小代价
ll calc(const vector<ll>& a, vector<ll> b) {
    int n = a.size();
    // cnt[x] = 在 a 前缀中出现次数 - 在 b 前缀中出现次数
    unordered_map<ll,int> cnt;
    cnt.reserve(n*2);
    ll diff = 0;   // 当前有多少个值的 cnt[x] != 0
    ll keep = 0;   // 可保留元素的总和
    
    for (int i = 0; i < n; i++) {
        // 插入 a[i]
        {
            ll x = a[i];
            int before = cnt[x];
            if (before != 0) diff--;     // 更新前差异计数
            int after = before + 1;
            cnt[x] = after;
            if (after != 0) diff++;      // 更新后差异计数
        }
        // 删除 b[i]
        {
            ll x = b[i];
            int before = cnt[x];
            if (before != 0) diff--;
            int after = before - 1;
            cnt[x] = after;
            if (after != 0) diff++;
        }
        // 若 diff == 0,说明前缀多重集合相同
        if (diff == 0 && a[i] == b[i]) {
            keep += a[i];
        }
    }
    return keep;
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n;
    cin >> n;
    vector<ll> a(n);
    ll sum = 0;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        sum += a[i];
    }
    // 升序
    vector<ll> b = a;
    sort(b.begin(), b.end());
    ll keep_asc = calc(a, b);
    // 降序
    sort(b.begin(), b.end(), greater<ll>());
    ll keep_desc = calc(a, b);
    // 最小代价 = 总和 - 最大保留和
    cout << (sum - keep_asc) << " " << (sum - keep_desc) << "\n";
    return 0;
}
python
# Python 实现
# 时间复杂度 O(n log n)
import sys
from collections import defaultdict
def calc(a, b):
    cnt = defaultdict(int)
    diff = 0
    keep = 0
    for x, y in zip(a, b):
        # a 前缀插入 x
        before = cnt[x]
        if before != 0: diff -= 1
        cnt[x] = before + 1
        if cnt[x] != 0: diff += 1
        # b 前缀移除 y
        before = cnt[y]
        if before != 0: diff -= 1
        cnt[y] = before - 1
        if cnt[y] != 0: diff += 1
        if diff == 0 and x == y:
            keep += x
    return keep
def main():
    input = sys.stdin.readline
    n = int(input())
    a = list(map(int, input().split()))
    total = sum(a)
    b = sorted(a)
    keep_asc = calc(a, b)
    b.reverse()
    keep_desc = calc(a, b)
    print(total - keep_asc, total - keep_desc)
if __name__ == "__main__":
    main()
java
// Java 实现
// 时间复杂度 O(n log n)
import java.io.*;
import java.util.*;
public class Main {
    // 计算 a 排成 b 的最大保留和
    static long calc(long[] a, long[] b) {
        Map<Long, Integer> cnt = new HashMap<>();
        long diff = 0, keep = 0;
        for (int i = 0; i < a.length; i++) {
            long x = a[i], y = b[i];
            // a 前缀插入 x
            int before = cnt.getOrDefault(x, 0);
            if (before != 0) diff--;
            int after = before + 1;
            cnt.put(x, after);
            if (after != 0) diff++;
            // b 前缀移除 y
            before = cnt.getOrDefault(y, 0);
            if (before != 0) diff--;
            after = before - 1;
            cnt.put(y, after);
            if (after != 0) diff++;
            // 若前缀一致且 a[i]==b[i],则可保留
            if (diff == 0 && x == y) {
                keep += x;
            }
        }
        return keep;
    }
    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(in.readLine());
        long[] a = new long[n];
        StringTokenizer st = new StringTokenizer(in.readLine());
        long sum = 0;
        for (int i = 0; i < n; i++) {
            a[i] = Long.parseLong(st.nextToken());
            sum += a[i];
        }
        long[] b = a.clone();
        Arrays.sort(b);  // 升序
        long keepAsc = calc(a, b);
        // 降序
        for (int i = 0; i < n/2; i++) {
            long t = b[i]; b[i] = b[n-1-i]; b[n-1-i] = t;
        }
        long keepDesc = calc(a, b);
        System.out.println((sum - keepAsc) + " " + (sum - keepDesc));
    }
}
        题目内容
小红有一个长度为 n 的数组 {a1,a2,...,an}。她可以对数组进行若干次如下操作,每次操作步骤:
- 
选取一个区间 [l,r],即子数组 {al,al+1,...,ar};
 - 
将该子数组中的所有元素以任意顺序重新排序;
 - 
本次操作需支付代价 ∑i=1rai=al+al+1+...+ar 。
 
请你分别计算出:
- 
进行若干次操作后,使得新的数组按从小到大排序的最小总代价;
 - 
进行若干次操作后,使得新的数组按从大到小排序的最小总代价。
 
输入描述
第一行输入一个整数 n(1≦n≦2×105) ,表示数组长度。
第二行输入 n 个整数 a1,a2,...,an(1≦ai≦109) ,老示数组元素。
输出描述
在一行上输出两个整数,分别表示将原数组按从小到大排序的最小总代价,将原数组按从大到小排序的最小总代价。
样例1
输入
3
1 2 4
输出
0 7
说明
从小到大的情况无需操作。
从大到小的情况需要选中整个数组,然后按照 [4,2,1] 排序,代价为 1+2+4=7 。