牛客练习赛114 解题报告 | 珂学家 | 贪心场 + 期望 + 线性基


前言

alt

只要坚信自己的道路,就无所谓天气是晴是雨。


整体评价

这场还是蛮难的,对我而言。C题写的有点变扭,到是D和F相对容易些。CDF都是基于贪心的解法,不过赛后看到了其他解法。G题属于高阶知识点,会的人觉得简单,不会的人基本没啥思路。


A. 光速签到

贪心,把大的数放在前面。

因为需要是10的倍数,而且允许前置0,所以只要保证至少一个0,就存在解了。

import java.io.BufferedInputStream;
import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(new BufferedInputStream(System.in));

        int n = sc.nextInt();
        int[] slot = new int[10];
        for (int i = 0; i < n; i++) {
            slot[sc.nextInt()]++;
        }
        if (slot[0] == 0) System.out.println("-1");
        else {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 10; i++) {
                for (int j = 0; j < slot[i]; j++) {
                    sb.append((char)(i + '0'));
                }
            }
            System.out.println(sb.reverse().toString());
        }

    }
}

B. 相信我,这真的是一个暴力!

如果知道 几何分布,那这题就非常的容易了。

先按照模拟来求解

如果, 则没有解,因为永远是平局

求一个极端概率, 则第i次的期望

因此理论上迭代100次就收敛了

import java.io.BufferedInputStream;
import java.util.Scanner;

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 a1 = sc.nextInt(), b1 = sc.nextInt(), c1 = sc.nextInt();
            int a2 = sc.nextInt(), b2 = sc.nextInt(), c2 = sc.nextInt();

            // 平局的概率
            int draw = a1 * a2 + b1 * b2 + c1 * c2;
            if (draw == 100) {
                System.out.println("Sorry,NoBruteForce");
            } else {
                double f = 1.0, p = draw / 100.0;
                double res = 0;

                // 迭代100次
                for (int i = 1; i <= 100; i++) {
                    res += i * f * (1 - p);
                    f = f * p;
                }
                System.out.println(res);

            }
        }
    }

}

如果知道 几何分布, 则期望

import java.io.BufferedInputStream;
import java.util.Scanner;

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 a1 = sc.nextInt(), b1 = sc.nextInt(), c1 = sc.nextInt();
            int a2 = sc.nextInt(), b2 = sc.nextInt(), c2 = sc.nextInt();

            // 平局的概率
            int draw = a1 * a2 + b1 * b2 + c1 * c2;
            if (draw == 100) {
                System.out.println("Sorry,NoBruteForce");
            } else {
                double p = draw / 100.0;
                System.out.println(1.0/(1.0 - p));
            }
        }
    }

}

C. Kevin的七彩旗

这题的核心目标就是

构建1~M的连续序列,求最小的切片数

可以先做预处理,就是把连续递增的子数组, 从原数组中切出来。

现在的问题就演变成, 对于一个区间集合 , 挑选最小的子集,能够构建的序列。

所以本质上,它是一个区间合并的最优化问题,先排序预处理。


方法一: 贪心构造

按左端点对区间进行排序

引入一个栈,栈中的元素是区间,自底向上彼此不相交,这一点很重要。

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();
            int[] arr = new int[n];
            for (int i = 0; i < n; i++) {
                arr[i] = sc.nextInt();
            }

            // 区间切割,保证 连续递增的子数组
            List<int[]> lists = new ArrayList<>();
            int j = 0;
            while (j < n) {
                if (arr[j] > m) {
                    j++;
                    continue;
                }
                int j2 = j + 1;
                while (j2 < n && arr[j2] == arr[j2 - 1] + 1 && arr[j2] <= m) {
                    j2++;
                }
                lists.add(new int[] {arr[j], arr[j2 - 1]});
                j = j2;
            }

            Collections.sort(lists, (a, b) -> {
                if (a[0] != b[0]) return a[0] < b[0] ? -1 : 1;
                if (a[1] != b[1]) return a[1] > b[1] ? -1 : 1;
                return 0;
            });

            Deque<int[]> deq = new ArrayDeque<>();
            for (int[] v: lists) {
                // 进行区间合并, 把之前小的进行覆盖
                boolean f = true;
                while (!deq.isEmpty()) {
                    int[] cur = deq.peekLast();
                    if (v[1] <= cur[1]) {
                        f = false;
                        break;
                    }
                    if (v[0] <= cur[0]) {
                        deq.pollLast();
                    } else {
                        break;
                    }
                }

                // 可以追加进去
                if (f) {
                    if (deq.isEmpty()) {
                        deq.offer(v);
                    } else {
                        deq.addLast(new int[] {Math.max(deq.peekLast()[1] + 1, v[0]), v[1]});
                    }
                }
            }

            // 进行最后的验证, 保证[1, m]完整
            int ans = deq.size();
            int next = 1;
            while (!deq.isEmpty()) {
                int[] cur = deq.pollFirst();
                if (cur[0] == next) {
                    next = cur[1] + 1;
                }
            }

            System.out.println(next > m ? ans : -1);
        }
    }

}

方法二:借助高级数据结构的DP

其实就是区间极值,像青蛙跳河系列的题

可以借助线段树,也可以借助非差分型的树状数组

import java.io.BufferedInputStream;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Supplier;

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();
            int[] arr = new int[n];
            for (int i = 0; i < n; i++) {
                arr[i] = sc.nextInt();
            }

            // 区间切割,保证 连续递增的子数组
            List<int[]> lists = new ArrayList<>();
            int j = 0;
            while (j < n) {
                if (arr[j] > m) {
                    j++;
                    continue;
                }
                int j2 = j + 1;
                while (j2 < n && arr[j2] == arr[j2 - 1] + 1 && arr[j2] <= m) {
                    j2++;
                }
                lists.add(new int[] {arr[j], arr[j2 - 1]});
                j = j2;
            }

            Collections.sort(lists, (a, b) -> {
                if (a[0] != b[0]) return a[0] < b[0] ? -1 : 1;
                if (a[1] != b[1]) return a[1] > b[1] ? -1 : 1;
                return 0;
            });

            RMQBIT<Integer> bit = new RMQBIT<>(m, () -> Integer.MAX_VALUE / 10, Math::min);
            for (int[] v: lists) {
                if (v[0] == 1) {
                    bit.set(v[1], 1);
                } else {
                    int r = bit.query(v[0] - 1, v[1] - 1);
                    bit.update(v[1], r + 1);
                }
            }

            // 进行最后的验证, 保证[1, m]完整
            int res = bit.query(m, m);
            System.out.println(res < Integer.MAX_VALUE / 10 ? res : -1);
        }
    }

    static
    public class RMQBIT<T> {
        int n;
        Object[] ori;
        Object[] arr;
        Supplier<T> e;
        BiFunction<T, T, T> op;

        public RMQBIT(int n, Supplier<T> e, BiFunction<T, T, T> op) {
            this.n = n;
            this.ori = new Object[n + 1];
            this.arr = new Object[n + 1];
            this.e = e;
            this.op = op;

            for (int i = 0; i <= n; i++) {
                this.ori[i] = e.get();
            }
            for (int i = 0; i <= n; i++) {
                this.arr[i] = e.get();
            }
        }

        public void setAll(T[] ori) {
            for (int i = 1; i <= n; i++) {
                this.ori[i] = this.arr[i] = ori[i];
                for (int j = 1; j < lowbit(i); j <<= 1) {
                    arr[j] = this.op.apply((T)arr[j], (T)arr[i - j]);
                }
            }
        }

        // O(log(n)), 前缀查询
        public T query(int p) {
            T res = (T)arr[p];
            while (p > 0) {
                res = this.op.apply(res, (T)arr[p]);
                p -= lowbit(p);
            }
            return res;
        }

        // 范围查询,操作时间复杂度, O(log(n)^2)
        public T query(int l, int r) {
            T ans = (T)ori[r];
            while (r >= l) {
                ans = this.op.apply(ans, (T)ori[r]);
                r--;
                while (r - l >= lowbit(r)) {
                    ans = this.op.apply(ans, (T)arr[r]);
                    r -= lowbit(r);
                }
            }
            return ans;
        }

        // O(log(n)), 符合单调性的更新
        public void update(int p, T v) {
            this.ori[p] = this.op.apply((T)this.ori[p], v);
            while (p <= n) {
                arr[p] = this.op.apply((T)arr[p], v);
                p += lowbit(p);
            }
        }

        // 操作时间复杂度, O(log(n)^2), 强制覆盖更新
        public void set(int p, T v) {
            this.ori[p] = v;
            for (; p <= n; p += lowbit(p)) {
                this.arr[p] = this.ori[p];
                for (int i = 1; i < lowbit(p); i <<= 1) {
                    this.arr[p] = this.op.apply((T)this.arr[p], (T)this.arr[p - i]);
                }
            }
        }

        int lowbit(int x) {
            return x & -x;
        }
    }

}



D. 长途的春天

可以先按值域统计,然后顺序遍历。

那核心的逻辑如何处理呢?

引入状态 表示i值结尾,长度为j的个数

状态转移为

// 优先去填充长度为1~4的状态
for (int j = 1; j <= 4; j++) {
    opt[i][j+1] = opt[i - 1][j] + min(opt[i - 1][j], nums[i]);
    nums[i] -= min(opt[i - 1][j], nums[i]);
}

// 5是缩点,代表>=5的状态
opt[i][5] = min(opt[i - 1][5], nums[i]);
nums[i] -= min(opt[i - 1][5], nums[i]);

// 最后有剩下的,才新建长度为1的顺子
opt[i][1] = nums[i];

核心逻辑是这个。

那为啥不叫这个是DP,而是贪心呢,因为这边局部最优和全局最优一致。

所以时间复杂度为, 空间复杂度为,不过这里空间上可以借助滚动数组优化。

如果值域很大怎么办? 没事,离散化处理一样成立。

import java.io.BufferedInputStream;
import java.util.Arrays;
import java.util.Scanner;

public class Main {

    static boolean solve(int n, int[] hash) {
        // 滚动数组优化
        int prev = 0, next = 1;
        int[][] opt = new int[2][6];

        for (int i = 1; i <= n; i++) {
            Arrays.fill(opt[next], 0);

            // 先填充长度为1~4的
            int v = hash[i];
            for (int j = 1; j <= 4; j++) {
                if (opt[prev][j] > 0) {
                    // 快速判定
                    if (opt[prev][j] > v) return false;
                    opt[next][j + 1] += opt[prev][j];
                    v -= opt[prev][j];
                }
            }
            // 填充5这个缩点
            if (v > 0 && opt[prev][5] > 0) {
                int d = Math.min(opt[prev][5], v);
                opt[next][5] += d;
                v -= d;
            }
            // 如果还有多余, 新建长度为1的顺子
            if (v > 0) {
                opt[next][1] = v;
            }
            prev = 1 - prev;
            next = 1 - next;
        }

        // 最后在判断一下尾巴
        for (int i = 1; i <= 4; i++) {
            if (opt[prev][i] > 0) return false;
        }
        return true;
    }

    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[] hash = new int[n + 1];
            for (int i = 0; i < n; i++) {
                int v = sc.nextInt();
                hash[v]++;
            }
            boolean res = solve(n, hash);
            System.out.println(res ? "YES" : "NO");
        }

    }

}

E. Kevin的抽奖黑幕

先占个位子


F. Kevin去砍树

这题很有意思,一开始往惯性上去思考,看看是否可以利用 并查集,可无单调栈。

但是好想又不是,总差点意思。

观察后发现

最优值一定是从山峰点出发

这个其实是可以反证的,如果该点不是山峰点,那起左侧或者右侧一定有它大的点.

不失一般性,假定是 左侧

按照流程约定,i点出发相当于失去了左侧的发展机会,而i-1点还包含了i在右侧的全部隐含收益。则

1, 2互相矛盾,则反证出,山峰点一定是最优解的出发点。

而山峰点的覆盖范围,只包含它邻近的非山峰点。

左侧是递增的,右侧是递减的。

唯一的约束是 左侧和右侧不能有相同高度的点。

所以这里,唯一的难点就在,那如何处理呢?

分别枚举左侧点和右侧点就行了,看看是否在另一侧存在。

其实每个点,都被枚举到了一次,山峰点代表区间,其他点枚举是相同点检测.

最后借助前缀和预处理,这题就呼之欲出了。

时间复杂度为

import java.io.BufferedInputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

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();

            int[] hs = new int[n];
            long[] ws = new long[n];

            // 前缀和预处理
            long[] prev = new long[n + 1];
            for (int i = 0; i < n; i++) {
                hs[i] = sc.nextInt();
            }
            for (int i = 0; i < n; i++) {
                ws[i] = sc.nextLong();
                prev[i + 1] = prev[i] + ws[i];
            }

            long ans = 0;
            for (int i = 0; i < n; i++) {

                // 山峰点判定
                if ((i == 0 && i + 1 < n && hs[i] >= hs[i + 1])
                        || (i > 0 && i + 1 < n && hs[i] >= hs[i - 1] && hs[i] >= hs[i + 1])
                        || (i > 0 && i + 1 == n && hs[i] >= hs[i - 1])
                ) {

                    // 寻找左侧的范围 (均摊为O(1))
                    int s = i, e = i;
                    Map<Integer, Integer> left = new HashMap<>();
                    Map<Integer, Integer> right = new HashMap<>();
                    while (s - 1 >= 0 && hs[s - 1] < hs[s]) {
                        s--;
                        left.put(hs[s], e);
                    }
                    while (e + 1 < n && hs[e + 1] < hs[e]) {
                        e++;
                        right.put(hs[e], e);
                    }

                    // 枚举左右侧,看是否在另一侧存在
                    long tmp = prev[i + 1] - prev[s];
                    ans = Math.max(ans, tmp);
                    for (int j = i + 1; j <= e; j++) {
                        if (left.containsKey(hs[j])) {
                            break;
                        }
                        tmp += ws[j];
                        ans = Math.max(ans, tmp);
                    }

                    tmp = prev[e + 1] - prev[i];
                    ans = Math.max(ans, tmp);
                    for (int j = i - 1; j >= s; j--) {
                        if (right.containsKey(hs[j])) {
                            break;
                        }
                        tmp += ws[j];
                        ans = Math.max(ans, tmp);
                    }
                } else {
                    ans = Math.max(ans, ws[i]);
                }
            }
            System.out.println(ans);
        }
    }

}

G. 图上异或难题

参考了大神的题解,先把边权转为点权

那问题就变成了数的集合,求子集的最大异或和。

借助线性基模板来求解

IO-WIKI上的线性基

import java.io.BufferedInputStream;
import java.util.Scanner;

public class Main {

    public static void insert(long[] vec, long v) {
        for (int i = 31; i >= 0; i--) {
            if ((v & (1 << i)) == 0) continue;
            if (vec[i]== 0) {
                vec[i] = v;
                return;
            }
            v ^= vec[i];
        }
    }

    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[] ws = new long[n + 1];
            for (int i = 0; i < m; i++) {
                int u = sc.nextInt(), v = sc.nextInt(), w = sc.nextInt();
                ws[u] ^= w;
                ws[v] ^= w;
            }

            // 构建向量基
            long[] vec = new long[32];
            for (int i = 1; i <= n; i++) {
                insert(vec, ws[i]);
            }

            // 获取子集异或和最大
            long ans = 0;
            for (int i = 31; i >= 0; i--) {
                ans = Math.max(ans, ans ^ vec[i]);
            }
            System.out.println(ans);
        }
    }

}

写在最后

就算我们不曾仰望,星空,也永远注视着我们。

alt

牛客练习赛解题报告 文章被收录于专栏

算法进阶,挑战下自己

全部评论

相关推荐

微风不断:兄弟,你把四旋翼都做出来了那个挺难的吧
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
10-05 10:13
已编辑
HHHHaos:让这些老登来现在秋招一下,简历都过不去
点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
11-26 16:06
已编辑
快手电商 后端 23k-35k
点赞 评论 收藏
分享
评论
5
2
分享
牛客网
牛客企业服务