牛客小白月赛74 解题报告 | 珂学家 | 二分+贪心+单调栈
前言
雪的碗里,盛的是月光。
整体评价
题目质量出的挺好的,可以一题多解,而且覆盖面也广,赞一个。
F题除了经典的二分,感觉直接贪心(类似最小生成树)也可以,而且是否可以借助可撤销的并查集来常数级优化,本文将给出解答。
G题是一道经典的单调栈优化的题,本质还是贪心,而且从思路上看和D题有一定的渊源。
A. 简单的整除
签到题,不细说了
import java.io.BufferedInputStream;
import java.util.Scanner;
public class A {
public static void main(String[] args) {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
int n = sc.nextInt();
if (n % 2 == 0 || n % 3 == 0 || n % 5 == 0 || n % 7 == 0) {
System.out.println("YES");
} else {
System.out.println("NO");
}
}
}
B. 整数划分
有意思的题,不过这里的字典序是自然序的意思
那就是一路贪心,知道没法生成连续的自然数为止
import java.io.BufferedInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
int kase = sc.nextInt();
while (kase-- > 0) {
int n = sc.nextInt();
List<Integer> res = new ArrayList<>();
for (int i = 1; n - i > i; i++) {
n -= i;
res.add(i);
}
res.add(n);
System.out.println(
res.stream().map(x -> String.valueOf(x)).collect(Collectors.joining(" "))
);
}
}
}
C. 传送阵
在同值无代价跳转,非同值,只能上下左右移动
看似是一个很复杂的问题,其实本质就是寻找不同值的个数
很好的一道思维题
import java.io.BufferedInputStream;
import java.util.Scanner;
import java.util.TreeSet;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
int kase = sc.nextInt();
while (kase-- > 0) {
int h = sc.nextInt();
int w = sc.nextInt();
TreeSet<Integer> st = new TreeSet<>();
for (int i = 0; i < h * w; i++) {
st.add(sc.nextInt());
}
System.out.println(st.size());
}
}
}
D. 修改后的和
经典的贪心问题
对于每个非负数的点,独立进行收益计算
然后排序(从大到小)进行最多m次贪心,这样求解下来的值,永远是最大的。
实际上贪心挑选的点集,并不影响实际的执行顺序,执行顺序一定还是倒序来执行的,即点集S中,最右边的一定先执行,最左边的最后执行。
这也是这道思维题,有点绕的地方.
import java.io.BufferedInputStream;
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
int kase = sc.nextInt();
while (kase-- > 0) {
int n = sc.nextInt(), m = sc.nextInt();
long[] arr = new long[n];
long acc = 0;
for (int i = 0; i < n; i++) {
arr[i] = sc.nextLong();
acc += arr[i];
}
// 挑选m个元素
List<Long> lists = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (arr[i] >= 0) {
lists.add(1l *arr[i] * (n - i));
}
}
Collections.sort(lists, Comparator.comparing(x -> -x));
for (int i = 0; i < m && i < lists.size(); i++) {
acc -= lists.get(i);
}
System.out.println(acc);
}
}
}
E. 幼稚园的树2
数据模拟题,需要分类讨论
这里面还有一个周期的概念,用于加速收敛
- 不需要剪枝,一直生长
- 需要剪枝,则进入一个特定生长周期
import java.io.BufferedInputStream;
import java.util.Scanner;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Main {
static long solve(long h, long inc, long m, long k, long b) {
if (h + inc * (m - 1) <= k) return h + inc * (m - 1);
long days = (k - h) / inc + 1;
long left = m - 1 - days;
if (left == 0) return b;
// 这个周期很重要
long round = (k - b + inc) / inc;
left %= round;
return b + left * inc;
}
public static void main(String[] args) {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
int kase = sc.nextInt();
while (kase-- > 0) {
int n = sc.nextInt();
int m = sc.nextInt();
int k = sc.nextInt();
int b = sc.nextInt();
int[] hs = new int[n];
int[] inc = new int[n];
for (int i = 0; i < n; i++) {
hs[i] = sc.nextInt();
}
for (int i = 0; i < n; i++) {
inc[i] = sc.nextInt();
}
System.out.println(
IntStream.range(0, n).mapToObj(i -> String.valueOf(solve(hs[i], inc[i], m, k,b)))
.collect(Collectors.joining(" "))
);
}
}
}
F. 最便宜的构建
很典的题,求满足连通图集合的前提下,满足的所有边集合S下,最大的边最小值是什么?
一般这种最大最小的问题,就用二分的思路。
因为是连通图校验,所以最自然的方式是,二分+并查集
方法一:二分+并查集
import java.io.BufferedInputStream;
import java.util.*;
public class Main {
static class Dsu {
int n;
int[] arr;
public Dsu(int n) {
this.n = n;
this.arr = new int[n + 1];
}
int find(int u) {
if (arr[u] == 0) return u;
return arr[u] = find(arr[u]);
}
void union(int u, int v) {
int ai = find(u);
int bi = find(v);
if (ai != bi) {
arr[ai] = bi;
}
}
}
static class Solution {
boolean check(int n, int[][] edges, int mid, int[][] sets) {
Dsu dsu = new Dsu(n);
for (int[] e: edges) {
if (e[2] > mid) break;
dsu.union(e[0], e[1]);
}
for (int[] set: sets) {
for (int j = 1; j < set.length;j++) {
if (dsu.find(set[0]) != dsu.find(set[j])) return false;
}
}
return true;
}
int solve(int n, int[][] edges, int[][] sets) {
Arrays.sort(edges, Comparator.comparing(x -> x[2]));
int m = edges.length;
int l = 0, r = m - 1;
int ans = 0;
while (l <= r) {
int mid = l + (r - l) / 2;
if (check(n, edges, edges[mid][2], sets)) {
ans = edges[mid][2];
r = mid - 1;
} else {
l = mid + 1;
}
}
return ans;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
int n = sc.nextInt();
int m = sc.nextInt();
int[][] edges = new int[m][3];
for (int i = 0; i < m; i++) {
edges[i][0] = sc.nextInt();
edges[i][1] = sc.nextInt();
edges[i][2] = sc.nextInt();
}
int s = sc.nextInt();
int[][] sets = new int[s][];
for (int i = 0; i < s; i++) {
int z = sc.nextInt();
sets[i] = new int[z];
for (int j = 0; j < z; j++) {
sets[i][j] = sc.nextInt();
}
}
Solution solution = new Solution();
System.out.println(solution.solve(n, edges, sets));
}
}
方法二:类最小生成树解法
是否可以利用最小生成树的方法来求解呢?
其实是可以的,这里面核心就是 验证连通性集合 这个能否优化
如果每加一条边,就验证一个集合所有元素,那这个成本/代价是在太高了.
但是这里面其实存在技巧
-
同一个集合的验证,不需要验证所有的点关系(),只要选择一个基准点(比如第0号元素),然后判定剩下的点()
-
如果前面的点已经判定OK了,那不需要重判定了
这样这个获选集的判定,是递增的,每一个关系判定均摊是O(1)的操作
这样总代价为
实际实现中,会引入下标对(a, b),对应sets集合的具体位子
所以贪心解,可能更快速
import java.io.BufferedInputStream;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Scanner;
public class Main {
static class Dsu {
int n;
int[] arr;
public Dsu(int n) {
this.n = n;
this.arr = new int[n + 1];
}
int find(int u) {
if (arr[u] == 0) return u;
return arr[u] = find(arr[u]);
}
void union(int u, int v) {
int ai = find(u);
int bi = find(v);
if (ai != bi) {
arr[ai] = bi;
}
}
}
static class Solution {
int solve(int n, int[][] edges, int[][] sets) {
Arrays.sort(edges, Comparator.comparing(x -> x[2]));
// 维护验证集合的下标 (a, b)
// a为第a个集合
// b为第a个集合的第(0, b)pair关系,是否满足连通性
// 这个(a, b)是递增的,而且前面的一旦满足,不需要再重复判定
// 一旦a >= len(sets), 就是满足的最小最大值
int a = 0, b = 1;
Dsu dsu = new Dsu(n);
for (int[] e: edges) {
int u = e[0], v = e[1], w = e[2];
dsu.union(u, v);
// 就是这一部分核心代码,看似while,则是是均摊O(1)的操作,理解就能懂了
while (a < sets.length) {
if (b >= sets[a].length) {
a++;
b = 1;
} else if (dsu.find(sets[a][0]) == dsu.find(sets[a][b])) {
b++;
} else {
break;
}
}
if (a >= sets.length) return w;
}
// unreachable
return -1;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
int n = sc.nextInt();
int m = sc.nextInt();
int[][] edges = new int[m][3];
for (int i = 0; i < m; i++) {
edges[i][0] = sc.nextInt();
edges[i][1] = sc.nextInt();
edges[i][2] = sc.nextInt();
}
int s = sc.nextInt();
int[][] sets = new int[s][];
for (int i = 0; i < s; i++) {
int z = sc.nextInt();
sets[i] = new int[z];
for (int j = 0; j < z; j++) {
sets[i][j] = sc.nextInt();
}
}
Solution solution = new Solution();
System.out.println(solution.solve(n, edges, sets));
}
}
G. 跳石头,搭梯子
一道非常有趣的题
这里面有几个有趣的发现
- 如果两个桥是包含关系,那么外面的桥一定优于里面的桥
- 不存在两个桥存在区间重叠(不包含端点),因为违反了桥的定义(即两端点>中间任意的点)
比如
这两个特性非常的重要,决定了这题的最终思路。
这里寻找桥的过程,可以借助单调栈来实现,但这个单调栈会产生存在包含关系的桥,和不存在重叠的桥(不包含端点)。
然后利用排序贪心,利用特性1,先找到最大的桥(最外面),然后过滤掉里面的桥即可。
这样挑选不超过M个的桥,就是最终的答案。
这里面还需要借助前缀和来加速,同时切记 不包含端点。
import java.io.BufferedInputStream;
import java.util.*;
public class Main {
static class Tx {
long v;
int s, e;
public Tx(long v, int s, int e) {
this.v = v;
this.s = s;
this.e = e;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(new BufferedInputStream(System.in));
int n = sc.nextInt();
int m = sc.nextInt();
List<Tx> cads = new ArrayList<>();
long[] pre = new long[n];
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = sc.nextInt();
if (i > 0) pre[i] = pre[i - 1] + Math.abs(arr[i] - arr[i - 1]);
}
// 单调栈的操作
Deque<int[]> deq = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
int v = arr[i];
int[] last = null;
while (!deq.isEmpty() && deq.peekLast()[1] < v) {
last = deq.pollLast();
}
if (!deq.isEmpty()) {
last = deq.peekLast();
}
if (last != null) {
// 获取一个候选的桥 区间
long rx = pre[i] - pre[last[0]] - Math.abs(v - last[1]);
cads.add(new Tx(rx, last[0], i));
}
deq.offer(new int[] {i, v});
}
// 贪心排序
cads.sort(Comparator.comparing(x -> -x.v));
// 利用treeset来实现,区间重叠的判定
// 即[s, e]只要有一个点被包含,即存在外面更大的桥(包含它),忽略即可
TreeSet<Integer> set = new TreeSet<>();
long ans = pre[n - 1];
for (Tx cur: cads) {
if (m <= 0) break;
int s = cur.s, e = cur.e;
Integer hk = set.ceiling(s);
if (hk != null && hk <= e) {
continue;
}
// 需要去掉端点
for (int i = s + 1; i < e; i++) {
set.add(i);
}
m--;
ans -= cur.v;
}
System.out.println(ans);
}
}
写在最后
此心想念你
碎成千片
我一片也不丢
牛客小白月赛解题报告系列,适合算法入门,也适合求职算法