牛客练习赛115 解题报告 | 珂学家 | 记忆化 + 斜率极值 + dfn序&树状数组


前言

alt


整体评价

比赛刚开始的时候,看到清一色的英语题目,就有种不祥的预感,果然......

感觉这场练习赛好难,在知识范围内是前四题,但是实际能ac 4题的却很少,是真的难。


A. Mountain sequence

要求构建一个山峰数组,求累计的方案总数

其实这题是构造题,按照要求确定山峰(最大值)后, 然后决策其他的数往那放置。

可以分组(按值)排序,然后从大到小放置。

对于某组(size=m), 则左右侧{(0, m), (1, m - 1), ..., (m, 0)} 总共m+1种方式。

所以这题显然易见,就是除最大数以外的组大小累乘。

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

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();
            TreeMap<Integer, Integer> hash = new TreeMap<>();
            for (int i = 0; i < n ;i++) {
                int v = sc.nextInt();
                hash.merge(v, 1, Integer::sum);
            }

            // treemap是保序的
            int[] arr = hash.values().stream().mapToInt(Integer::valueOf).toArray();

            long mod = 998244353l;
            long ans = 1l;
            // 这边需要把最大数的组(山峰)去掉, 所以枚举到[0, arr.length - 1)
            for (int i = 0; i < arr.length - 1; i++) {
                int m = arr[i];
                ans = ans * (m + 1) % mod;
            }

            System.out.println(ans);
        }
    }

}

B. Antiamuny wants to leaern binary search again

既然是通过次数来求可能的目标值,那完全可以逆序模拟二分.

1. 模拟二分+贪心

二分区间的时候,右侧的区间更大一些,所以尽量往右侧寻找。

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 l = sc.nextInt(), r = sc.nextInt();
            int cnt = sc.nextInt();

            int ans = -1;
            while (l <= r) {
                int m = l + (r - l) / 2;
                cnt--;
                if (cnt == 0) {
                    ans = m;
                    break;
                }
                // 贪心走右侧
                l = m + 1;
            }
            System.out.println(ans);
        }
    }

}

2. 记忆化

因为这题范围为, 而

这题也可以用dp的方式求解,不过记忆化的话,更容易书写

, m表示长度,s表示次数,存在的情况对应的数字(偏移)

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

public class Main {

    static Integer[][] opt = new Integer[100_0001][21];

    static int dfs(int l, int r, int z) {
        if (r < l) return -1;

        int span = r - l + 1;
        int mid = l + (r - l) / 2;
        if (z == 1) {
            return mid;
        }

        if (opt[span][z] != null) {
            return opt[span][z] == -1 ? -1 : opt[span][z] + l;
        }

        int r1 = dfs(l, mid - 1, z - 1);
        if (r1 != -1) {
            opt[span][z] = r1 - l;
            return r1;
        }

        int r2 = dfs(mid + 1, r, z - 1);
        if (r2 != -1) {
            opt[span][z] = r2 - l;
            return r2;
        }

        opt[span][z] = -1;
        return -1;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(new BufferedInputStream(System.in));
        int kase = sc.nextInt();
        while (kase-- > 0) {
            int l = sc.nextInt(), r = sc.nextInt();
            int cnt = sc.nextInt();
            System.out.println(dfs(l, r, cnt));
        }
    }

}

C. Find the maximum slope

alt

如果把这个公式,看做斜率k=(arr[i] - arr[j])/(i-j)

那这个斜率极值(最大/最小),必然是相邻。

反证法,假如不是相邻的元素构成的斜率最大。

alt

结合数形,可以发现,除非在一条直线上,否则必然能找到一条更大的斜率.

因此,我们可以证明:

最大值,一定是相邻元素构造的。

那剩下的事情,就简单很多了。

按要求构建相邻两元素的差分数组。

对于区间修改,只会影响区间的边界两个点差分值。

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

public class Main {

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

        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = sc.nextInt();
        }

        // treemap是维护差分的最大值
        TreeMap<Integer, Integer> range = new TreeMap<>();
        int[] diff = new int[n - 1];
        for (int i = 0; i < n - 1; i++) {
            diff[i] = arr[i] - arr[i + 1];
            range.merge(Math.abs(diff[i]), 1, Integer::sum);
        }

        System.out.println(range.lastKey());
        for (int i = 0; i < q; i++) {
            int l = sc.nextInt() - 1, r = sc.nextInt() - 1;
            int x = sc.nextInt();

            // 区间修改只会影响2个端边界
            if (l > 0) {
                range.computeIfPresent(Math.abs(diff[l - 1]), (a, b) -> b > 1 ? b - 1 : null);
                diff[l - 1] -= x;
                range.merge(Math.abs(diff[l - 1]), 1, Integer::sum);
            }

            if (r + 1 < n) {
                range.computeIfPresent(Math.abs(diff[r]), (a, b) -> b > 1 ? b - 1 : null);
                diff[r] += x;
                range.merge(Math.abs(diff[r]), 1, Integer::sum);
            }
            System.out.println(range.lastKey());
        }

    }

}

这题,出题者真的太贼了,故意引入浮点数,限定值域大小,不知道多少小伙伴直接被带偏了。


D. Genealogy in the trees

因为涉及树上的祖先判定,因此dfn序是免不了的。

dfn序之后,该如何求解,成了这题的关键。

这题应该非常的经典,一种思路借鉴了何老师的做法,时间序模拟操作,树状数组支持区间查询。

另一种是 树上启发式合并,需要AVL树的支持。


1. 树状数组

预处理dfn时间序,每个对都有一个start, end时间戳

按照离线计算的思路,再构建一个操作序列

  • 点对按p,的时间序start加入,按end离开, 进行bit操作
  • 查询按a,的时间点,进行bit查询

这题数据量有点大,需要用快读模板。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.*;
import java.util.stream.Collectors;

public class Main {

    static class BIT {
        int n;
        int[] arr;
        public BIT(int n) {
            this.n = n;
            this.arr = new int[n + 1];
        }
        public void update(int p, int d) {
            while (p <= n) {
                arr[p] += d;
                p += p & -p;
            }
        }
        public int query(int p) {
            int res = 0;
            while (p > 0) {
                res += arr[p];
                p -= p & -p;
            }
            return res;
        }
    }

    static class Solution {

        int timestamp = 0;
        int[] start, end;

        int n;
        List<Integer> []g;

        int[] solve(int n, int[][] es, int[][] ms, int[][] qs) {
            this.n = n;
            this.start = new int[n + 1];
            this.end = new int[n + 1];

            this.g = new List[n + 1];
            Arrays.setAll(g, x -> new ArrayList<>());

            for (int i = 0; i < es.length; i++) {
                int u = es[i][0], v = es[i][1];
                g[u].add(v);
                g[v].add(u);
            }

            // 有根树,root=1
            dfs(1, -1);

            // 离线计算,转换为操作序列
            // 三元组 (time, type, time/id)
            List<int[]> ops = new ArrayList<>();
            for (int i = 0; i < ms.length; i++) {
                int p = ms[i][0], q = ms[i][1];
                ops.add(new int[] {start[p], 1, start[q]});
                ops.add(new int[] {end[p], -1, start[q]});
            }
            for (int i = 0; i < qs.length; i++) {
                int a = qs[i][0];
                ops.add(new int[] {start[a], 0, i});
            }

            ops.sort((x, y) -> {
                int r = Integer.compare(x[0], y[0]);
                if (r != 0) return r;
                return -Integer.compare(Math.abs(x[1]), Math.abs(y[1]));
            });

            int[] res = new int[qs.length];
            BIT bit = new BIT(timestamp);
            for (int[] e: ops) {
                int type = e[1];
                if (type == 0) {
                    int idx = e[2];
                    int b = qs[idx][1];
                    res[idx] = bit.query(end[b]) - bit.query(start[b] - 1);
                } else {
                    bit.update(e[2], e[1]);
                }
            }

            return res;
        }

        // 做dfn序
        void dfs(int u, int fa) {
            start[u] = ++timestamp;
            for (int v: g[u]) {
                if (v == fa) continue;
                dfs(v, u);
            }
            end[u] = ++timestamp;
        }

    }

    public static void main(String[] args) {
        AReader sc = new AReader();
        int n = sc.nextInt(), m = sc.nextInt(), q = sc.nextInt();

        int[][] es = new int[n - 1][2];
        for (int i = 0; i < n - 1; i++) {
            es[i][0] = sc.nextInt();
            es[i][1] = sc.nextInt();
        }
        int[][] ms = new int[m][2];
        for (int i = 0; i < m; i++) {
            ms[i][0] = sc.nextInt();
            ms[i][1] = sc.nextInt();
        }
        int[][] qs = new int[q][2];
        for (int i = 0; i < q; i++) {
            qs[i][0] = sc.nextInt();
            qs[i][1] = sc.nextInt();
        }

        Solution solution = new Solution();
        int[] res = solution.solve(n, es, ms, qs);
        System.out.println(Arrays.stream(res).mapToObj(String::valueOf)
                           .collect(Collectors.joining("\n")));
    }

    static
    class AReader {
        private BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        private StringTokenizer tokenizer = new StringTokenizer("");
        private String innerNextLine() {
            try {
                return reader.readLine();
            } catch (IOException ex) {
                return null;
            }
        }
        public boolean hasNext() {
            while (!tokenizer.hasMoreTokens()) {
                String nextLine = innerNextLine();
                if (nextLine == null) {
                    return false;
                }
                tokenizer = new StringTokenizer(nextLine);
            }
            return true;
        }
        public String nextLine() {
            tokenizer = new StringTokenizer("");
            return innerNextLine();
        }
        public String next() {
            hasNext();
            return tokenizer.nextToken();
        }
        public int nextInt() {
            return Integer.parseInt(next());
        }

        public long nextLong() {
            return Long.parseLong(next());
        }
        
    }

}

2. 启发式合并

先占个坑,需要名次树支持

dfn时间序做预处理

离线计算做法,每个节点有两个名次数,一个维护前缀(节点的start时间戳),一个维护后缀(节点的end时间戳)

自底向上,做启发式合并

很像LCA的tarjan解法,即在dfs过程中,离线把结果计算了。

前缀rank - 后缀rank


E. High contrast pattern


F. Jewel maximizing magic


写在最后

alt

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

算法进阶,挑战下自己

全部评论

相关推荐

11-28 17:58
门头沟学院 Java
美团 JAVA开发 n×15.5
牛客786276759号:百度现在晋升很难的 而且云这块的业务没美团好 你看百度股价都跌成啥样了
点赞 评论 收藏
分享
评论
4
2
分享
牛客网
牛客企业服务