#P3392. 第1题-减小逆序对
-
1000ms
Tried: 62
Accepted: 19
Difficulty: 5
所属公司 :
京东
时间 :2025年8月16日
-
算法标签>思维
第1题-减小逆序对
思路与结论
-
只有“跨区间”的数对会受影响:
- 新增逆序对:区间内的元素与区间右侧相同值形成的新逆序对(ai==aj 且 i 在区间内、j 在区间右侧)。
- 被消去的逆序对:区间左侧值为 v+1 与区间内值为 v 的数对(ap=aq+1,p 在区间左侧,q 在区间内)。
-
将“选择区间 [L,R] 并把区间内加 1”的收益定义为:
- 收益 = 被消去对数 − 新增对数
- 等价于:sumq∈[L,R]pre[L−1][aq+1] − sumq∈[L,R]suf[R+1][aq]
-
线性求解(关键技巧):
-
右端点R一定要放在末尾,因为如果放在其它位置,右端点后的数可能会和区间里的数形成额外的逆序对
-
从右到左把区间“向左扩展”。用两个数组维护:
rest[v]:当前还未划入区间/未处理的元素中,值为 v 的个数。put[v]:已放入区间且“被视作 +1 后”的值为 v 的个数(即原来是 v-1)。
-
当把位置 i 放入区间时(把 a[i] 视为被 +1):
- 被消去对数增加
rest[a[i]+1](区间左侧与当前值成 (v+1, v))。 - 新增对数增加
put[a[i]](当前值与右侧区间已放入、加一后的相同值)。 - 将当前元素“记到加一后”的桶:
put[a[i]+1] += 1。
- 被消去对数增加
-
每步维护当前收益前缀的最大值,即为答案。
-
复杂度
- 时间复杂度:O(n) 每组(值域 ≤ n,数组统计)
- 空间复杂度:O(n)
Python 实现
import sys
def solve():
it = iter(sys.stdin.read().strip().split())
t = int(next(it))
out = []
for _ in range(t):
n = int(next(it))
a = [int(next(it)) for _ in range(n)]
# 值域 ≤ n,开到 n+2 防止 a[i]+1 溢出
cap = n + 2
# rest[v]: 还未处理元素中值为 v 的个数(初始全体)
rest = [0] * (cap + 1)
for x in a:
rest[x] += 1
# put[v]: 已加入区间、并视作 +1 后的值为 v 的个数(原始值为 v-1)
put = [0] * (cap + 1)
cur = 0 # 当前把区间向左扩的累计收益
best = 0 # 最大收益
# 从右到左枚举左端点
for i in range(n - 1, -1, -1):
x = a[i]
rest[x] -= 1
# 增量 = 被消去对数 - 新增对数
cur += rest[x + 1]
cur -= put[x]
# 把当前元素并入区间(加一后记到 x+1)
put[x + 1] += 1
if cur > best:
best = cur
out.append(str(best))
sys.stdout.write("\n".join(out))
if __name__ == "__main__":
solve()
Java 实现
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringBuilder sb = new StringBuilder();
int T = Integer.parseInt(br.readLine().trim());
while (T-- > 0) {
int n = Integer.parseInt(br.readLine().trim());
int[] a = new int[n];
{
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 0; i < n; i++) a[i] = Integer.parseInt(st.nextToken());
}
int cap = n + 2;
int[] rest = new int[cap + 1]; // 未处理计数
int[] put = new int[cap + 1]; // 已入区间且视作+1后的计数
for (int x : a) rest[x]++;
long cur = 0; // 当前累计收益
long best = 0; // 最大收益
for (int i = n - 1; i >= 0; i--) {
int x = a[i];
rest[x]--;
cur += rest[x + 1]; // 被消去对数增加
cur -= put[x]; // 新增对数增加
put[x + 1]++; // 当前元素加入区间(视作 +1)
if (cur > best) best = cur;
}
sb.append(best).append('\n');
}
System.out.print(sb.toString());
}
}
C++ 实现
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
if (!(cin >> T)) return 0;
while (T--) {
int n; cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
int cap = n + 2;
vector<long long> rest(cap + 1, 0), put(cap + 1, 0);
for (int x : a) rest[x]++;
long long cur = 0, best = 0;
for (int i = n - 1; i >= 0; --i) {
int x = a[i];
rest[x]--;
cur += rest[x + 1]; // 被消去对数
cur -= put[x]; // 新增对数
put[x + 1]++; // 进入区间后视作 +1
if (cur > best) best = cur;
}
cout << best << "\n";
}
return 0;
}
题目内容
你有一个长度为 n 的序列 A:a1,a2,…,an,你需要进行如下操作至多一次:
选择一个区间[L,R](1≤L≤R≤n),将 a1,aL+1,...,aR 全部加 1 。即 aL,aL+1,...,aR 均变为 aL+1,aL+1+1,...,aR+1
设变化后的序列为 B:b1,b2,…,bn,你需要最大化 inv(A)−inv(B) ,其中 inv() 函数表示该序列的逆序对数量,即满足 i<j,a1>aj 的二元组 (i,j) 个数。换句话说,你需要尽可能地减小序列 A 的逆序对数量。
输入描述
第一行一个正整数 T ,表示数据组数;
对于每一组数据,第一行一个正整数 n ; 第二行 n 个正整数 a1,a2,...,an,表示序列 A 。
1≤n≤105,1≤T≤5,1≤ai≤n
输出描述
对于每一组数据,输出一个整数,表示最大的 inv(A)−inv(B) 。
样例1
输入
3
5
4 2 3 1 5
5
1 2 3 4 5
3
2 1 1
输出
2
0
2
说明
第一组数据,选择区间 [3,4] ,变化后的序列为 [4,2,4,2,5,5] ,逆序对数量为3;变化前逆序对数量为 5 。
第二组数据,选择任意区间都不会减小逆序对数量。
第三组数据,选择区间 [2,3] ,变化后的序列为 [2,2,2],逆序对数量为 0 ;变化前逆序对数量为 2 。