#P3361. 第1题-排列最小化
-
2000ms
Tried: 86
Accepted: 32
Difficulty: 4
所属公司 :
米哈游
时间 :2025年8月10日
-
算法标签>贪心算法
第1题-排列最小化
解题思路
核心算法
使用贪心策略结合位置映射表,从左到右逐个位置确保放置最优值。
关键观察
- 字典序最小的本质:要使字典序最小,应该让前面的位置尽可能放置小的数字
- 最优排列的特征:理想情况下,位置i(0-indexed)应该放置值i+1
- 贪心策略的正确性:优先处理前面的位置,因为前面位置对字典序的影响更大
算法设计思路
贪心策略
从左到右遍历每个位置,对于位置i:
- 如果当前位置a[i]已经等于理想值i+1,无需操作
- 如果a[i] ≠ i+1,需要找到值为i+1的元素并交换到位置i
- 每次交换消耗一次操作机会
高效查找机制
为了避免每次都线性搜索目标值的位置,采用位置映射表:
- 预处理阶段:建立pos[value] = index的映射关系
- 查找阶段:通过pos[target]直接获取目标位置
- 维护阶段:每次交换后同步更新映射表
具体执行步骤
- 初始化:创建位置映射表pos,使得pos[a[i]] = i
- 贪心处理:从位置0开始逐个处理
- 对于位置i,理想值为target = i+1
- 如果a[i] ≠ target且还有交换次数,执行交换
- 找到target所在位置j = pos[target]
- 更新映射表:pos[a[i]] = j, pos[target] = i
- 交换a[i]和a[j],交换次数减1
- 终止条件:交换次数用完或已达到最优状态
代码实现
Python
def solve():
# 读取输入
n, m = map(int, input().split())
a = list(map(int, input().split()))
# 建立值到位置的映射
pos = [0] * (n + 1) # pos[value] = index
for i in range(n):
pos[a[i]] = i
# 贪心策略:从左到右为每个位置放置最优值
for i in range(n):
# 如果交换次数用完,直接退出
if m == 0:
break
# 如果当前位置不是最优值(应该是i+1)
target = i + 1
if a[i] != target:
# 通过映射表直接找到目标值的位置
j = pos[target]
# 交换前先更新位置映射
pos[a[i]] = j
pos[a[j]] = i
# 交换当前位置和目标位置
a[i], a[j] = a[j], a[i]
m -= 1 # 消耗一次交换机会
# 输出结果
print(' '.join(map(str, a)))
# 处理多组测试数据
T = int(input())
for _ in range(T):
solve()
Java
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
// 处理多组测试数据
while (T-- > 0) {
// 读取输入
int n = sc.nextInt();
int m = sc.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
}
// 建立值到位置的映射
int[] pos = new int[n + 1]; // pos[value] = index
for (int i = 0; i < n; i++) {
pos[a[i]] = i;
}
// 贪心策略:从左到右为每个位置放置最优值
for (int i = 0; i < n && m > 0; i++) {
// 如果当前位置不是最优值(应该是i+1)
int target = i + 1;
if (a[i] != target) {
// 通过映射表直接找到目标值的位置
int j = pos[target];
// 交换前先更新位置映射
pos[a[i]] = j;
pos[target] = i;
// 交换当前位置和目标位置
int temp = a[i];
a[i] = a[j];
a[j] = temp;
m--; // 消耗一次交换机会
}
}
// 输出结果
for (int i = 0; i < n; i++) {
System.out.print(a[i]);
if (i < n - 1) System.out.print(" ");
}
System.out.println();
}
sc.close();
}
}
C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int T;
cin >> T;
// 处理多组测试数据
while (T--) {
// 读取输入
int n, m;
cin >> n >> m;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
// 建立值到位置的映射
vector<int> pos(n + 1); // pos[value] = index
for (int i = 0; i < n; i++) {
pos[a[i]] = i;
}
// 贪心策略:从左到右为每个位置放置最优值
for (int i = 0; i < n && m > 0; i++) {
// 如果当前位置不是最优值(应该是i+1)
int target = i + 1;
if (a[i] != target) {
// 通过映射表直接找到目标值的位置
int j = pos[target];
// 交换前先更新位置映射
pos[a[i]] = j;
pos[target] = i;
// 交换当前位置和目标位置
swap(a[i], a[j]);
m--; // 消耗一次交换机会
}
}
// 输出结果
for (int i = 0; i < n; i++) {
cout << a[i];
if (i < n - 1) cout << " ";
}
cout << "\n";
}
return 0;
}
题目内容
给定一个长度为 n 的排列 {a1,a2,...,an} 。
你可以进行至多 m 次以下操作:
- 选择两个索引 i,j,交换 ai 与 aj 。
请输出在经过至多 m 次交换操作后能够得到的字典序最小的排列。
【名词解释】
排列长度为 n 的排列是由 1 ~ n 这 n 个整数、按任意顺序组成的数组,其中每个整数恰好出现一次。例如,{ 2,3,1,5,4 }是一个长度为 5 的排列,而{ 1,2,2 }和{ 1,3,4 }都不是排列,因为前者存在重复元素,后者包含了超出范围的数。
字典顺序比较:从数组的第一个元素开始逐个比较,直到找到第一个不同的位置,通过比较这个位置元素的大小得出数组的大小,称为字典顺序比较。
输入描述
每个测试文件均包含多组测试数据。
第一行输入一个整数 T(1≤T≤104) ,代表数据组数;
随后对于每组数据,按以下格式输入:
- 第一行输入两个整数
n,m(1≦n≦2×105,1≦m≤109),分别表示排列长度和允许的交换次数;
- 第二行输入一个长度为 n 的排列
{ a1,a2,...,an } (1≤ai≤n) 。
保证所有测试数据中 n 的总和不超过 2×105 。
输出描述
对于每组测试数据,在一行上输出一个长度为 n 的排列,表示经过至多 m 次交换操作后得到的字典序最小的排列。
样例1
输入
2
2 1
2 1
3 4
3 2 1
输出
1 2
1 2 3
说明
在第二个测试用例中,可以选择交换 (i,j)=(1,3) ,将排列 { 3,2,1 }变为{ 1,2,3 }。