文章目录
题目描述与示例
题目描述
给定两个只包含数字的数组 a
, b
,调整数组 a
里面数字的顺序,使得尽可能多的 a[i] > b[i]
。数组 a
和 b
中的数字各不相同。
输出所有可以达到最优结果的 a
数组数量
输入描述
输入的第一行是数组 a
中的数字
输入的第二行是数组 b
中的数字
其中只包含数字,每两个数字之间相隔一个空格,a
,b
数组大小不超过 10
输出描述
输出所有可以达到最优结果的 a
数组数量
示例一
输入
11 8 20
10 13 7
输出
1
说明
最优结果只有一个,a = [11, 20, 8]
,故输出 1
示例二
输入
11 12 20
10 13 7
输出
2
说明
有两个 a
数组的排列可以达到最优结果,[12, 20, 11]
和[11, 20, 12]
,故输出 2
示例三
输入
1 2 3
4 5 6
输出
0
说明
a
无论如何都会全输,所以无最优解。
解题思路
最优结果的胜出组数
经过调整后,a
数组中胜利的最大组数max_win_num
,很容易想到用贪心来解决。
max_win_num = 0
a.sort(reverse=True)
b.sort(reverse=True)
idx_a, idx_b = 0, 0
while idx_b < n:
if a[idx_a] > b[idx_b]:
idx_a += 1
idx_b += 1
max_win_num += 1
else:
idx_b += 1
达到最优结果的数组数量
注意到a
,b
数组大小不超过 10
,很容易想到可以用回溯枚举的方式,列举出所有具有max_win_num
组胜利的数组a
的排列。
这里的回溯无非是在数组a
的全排列的基础上,加上若干题意的限制条件。
数组大小最大为10
,一共存在10! = 3628800 ~= 10^6
种排列情况,如果不加以剪枝,全部枚举出来,可能会导致超时。
对于每一层递归,考虑剩余可选择的元素个数rest_not_used = used.count(False)
。本题存在两种剪枝策略:
- 当
rest_not_used
个元素全部都选上,数组a
的胜利总组数rest_not_used + win_num
都无法到达max_win_num
组时,在当前状态继续进行递归已经没有意义了,可以直接剪枝 - 当当前胜利组数已经等于最大胜利组数,即
win_num == max_win_num
成立时,剩余的rest_not_used
个元素一共可以带来factorial(rest_not_used)
种排列,无需通过回溯获得这些排列的具体结果,直接令答案数量加上factorial(rest_not_used)
即可。
from math import factorial
def dfs(a, b, idx_b, n, win_num, used):
global ans
rest_not_used = used.count(False)
if max_win_num > win_num + rest_not_used:
return
if win_num == max_win_num:
ans += factorial(rest_not_used)
return
for idx_a in range(n):
if used[idx_a]:
continue
used[idx_a] = True
dfs(a, b, idx_b+1, n, win_num + int(a[idx_a] > b[idx_b]), used)
used[idx_a] = False
代码
Python
# 题目:【回溯】2023C-田忌赛马
# 分值:200
# 作者:许老师-闭着眼睛学数理化
# 算法:贪心/回溯
# 代码看不懂的地方,请直接在群上提问
# 导入计算阶乘的函数
from math import factorial
def dfs(a, b, idx_b, n, win_num, used):
global ans
# 数组a中,还没选择的个数rest_not_used
# 即为used中值为False的个数
# 或者n-idx_b,也是rest_not_used的值
rest_not_used = used.count(False)
# 剪枝:
# 如果rest_not_used个元素全部都选上
# 都无法到达max_win_num组,则无需继续考虑,直接剪枝
if max_win_num > win_num + rest_not_used:
return
# 如果当前排列能够胜利的组数win_num等于max_win_num
# 说明剩余尚未选择的rest_not_used个元素,
# 无论如何排列,都一定是一组要求的答案
# 根据排列组合,剩余的rest_not_used个元素,
# 一共有factorial(rest_not_used)中排列方式
if win_num == max_win_num:
ans += factorial(rest_not_used)
return
# 考虑所有位置
for idx_a in range(n):
# 如果已经使用,则直接跳过
if used[idx_a]:
continue
# 状态更新
used[idx_a] = True
# a[idx_a]和b[idx_b]进行比较
# 如果前者大于后者,则win_num需要+1,否则不变
# 同时idx_b需要+1
dfs(a, b, idx_b+1, n, win_num + int(a[idx_a] > b[idx_b]), used)
# 回滚
used[idx_a] = False
# 输入a,b数组
a = list(map(int, input().split()))
b = list(map(int, input().split()))
n = len(a)
# 基于贪心思想,计算出a数组经过排序之后的最大胜利组数
max_win_num = 0
# 对数组a、b进行从大到小的排序
a.sort(reverse=True)
b.sort(reverse=True)
# 初始化两个指针,分别指向a和b的开头位置
idx_a, idx_b = 0, 0
# 循环
# 贪心策略:拿当前a中最大的值a[idx_a]
# 和b中小于a[idx_a]的数中的最大值,进行配对
while idx_b < n:
# 如果当前a的最大值大于当前b的最大值
# 让a[idx_a]和b[idx_b]进行匹配
if a[idx_a] > b[idx_b]:
idx_a += 1
idx_b += 1
max_win_num += 1
# 否则让idx_b右移,寻找一个能够跟a[idx_a]匹配的最大值
else:
idx_b += 1
ans = 0
# 如果a全输,则没有最优策略,直接输出0
if max_win_num == 0:
print(ans)
# 否则,才有进行回溯,计算具有最优策略的a数组排列的数量
else:
used = [False] * n
dfs(a, b, 0, n, 0, used)
print(ans)
Java
import java.util.*;
public class Main {
static int maxWinNum = 0;
static int ans = 0;
static void dfs(ArrayList<Integer> a, ArrayList<Integer> b, int idxB, int n, int winNum, boolean[] used) {
int restNotUsed = 0;
for (boolean val : used) {
if (!val) {
restNotUsed++;
}
}
if (maxWinNum > winNum + restNotUsed) {
return;
}
if (winNum == maxWinNum) {
ans += factorial(restNotUsed);
return;
}
for (int idxA = 0; idxA < n; ++idxA) {
if (!used[idxA]) {
used[idxA] = true;
dfs(a, b, idxB + 1, n, winNum + (a.get(idxA) > b.get(idxB) ? 1 : 0), used);
used[idxA] = false;
}
}
}
static int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
ArrayList<Integer> a = new ArrayList<>();
ArrayList<Integer> b = new ArrayList<>();
String[] inputs = scanner.nextLine().split(" ");
for (String input : inputs) {
a.add(Integer.parseInt(input));
}
inputs = scanner.nextLine().split(" ");
for (String input : inputs) {
b.add(Integer.parseInt(input));
}
int n = a.size();
boolean[] used = new boolean[n];
Collections.sort(a, Collections.reverseOrder());
Collections.sort(b, Collections.reverseOrder());
int idxA = 0, idxB = 0;
while (idxB < n) {
if (a.get(idxA) > b.get(idxB)) {
idxA++;
idxB++;
maxWinNum++;
} else {
idxB++;
}
}
if (maxWinNum == 0) {
System.out.println(ans);
} else {
dfs(a, b, 0, n, 0, used);
System.out.println(ans);
}
}
}
C++
#include <iostream>
#include <sstream>
#include <vector>
#include <algorithm>
using namespace std;
int maxWinNum = 0;
int ans = 0;
void dfs(vector<int>& a, vector<int>& b, int idxB, int n, int winNum, vector<bool>& used) {
int restNotUsed = 0;
for (bool val : used) {
if (!val) {
restNotUsed++;
}
}
if (maxWinNum > winNum + restNotUsed) {
return;
}
if (winNum == maxWinNum) {
int fact = 1;
for (int i = 1; i <= restNotUsed; ++i) {
fact *= i;
}
ans += fact;
return;
}
for (int idxA = 0; idxA < n; ++idxA) {
if (!used[idxA]) {
used[idxA] = true;
dfs(a, b, idxB + 1, n, winNum + (a[idxA] > b[idxB] ? 1 : 0), used);
used[idxA] = false;
}
}
}
int main() {
vector<int> a, b;
string input;
getline(cin, input);
stringstream stream(input);
int num;
while (stream >> num) {
a.push_back(num);
}
getline(cin, input);
stringstream stream2(input);
while (stream2 >> num) {
b.push_back(num);
}
int n = a.size();
vector<bool> used(n, false);
sort(a.begin(), a.end(), greater<int>());
sort(b.begin(), b.end(), greater<int>());
int idxA = 0, idxB = 0;
while (idxB < n) {
if (a[idxA] > b[idxB]) {
idxA++;
idxB++;
maxWinNum++;
} else {
idxB++;
}
}
if (maxWinNum == 0) {
cout << ans << endl;
} else {
dfs(a, b, 0, n, 0, used);
cout << ans << endl;
}
return 0;
}
时空复杂度
时间复杂度:O(N*N!)
。由于剪枝的存在,这是一个宽松的上界。状态树的深度最大为N
,一共有N!
种不同的排列。
空间复杂度:O(N)
。
华为OD算法/大厂面试高频题算法练习冲刺训练
-
华为OD算法/大厂面试高频题算法冲刺训练目前开始常态化报名!目前已服务100+同学成功上岸!
-
课程讲师为全网50w+粉丝编程博主@吴师兄学算法 以及小红书头部编程博主@闭着眼睛学数理化
-
每期人数维持在20人内,保证能够最大限度地满足到每一个同学的需求,达到和1v1同样的学习效果!
-
60+天陪伴式学习,40+直播课时,300+动画图解视频,300+LeetCode经典题,200+华为OD真题/大厂真题,还有简历修改、模拟面试、专属HR对接将为你解锁
-
可上全网独家的欧弟OJ系统练习华子OD、大厂真题
-
可查看链接 大厂真题汇总 & OD真题汇总(持续更新)
-
绿色聊天软件戳
od1336
了解更多