2026-03-28 某外卖厂暑期笔试复盘

Published 2026-03-28 04:00 1967 words 10 min read Handwritten Every word here was typed by me.

Zhiheng Wang avatar

Zhiheng Wang

技术文章、个人随笔、项目与作品体验记录

This post is not yet available in English. Showing the original.
2026-03-28 某外卖厂暑期笔试复盘 第一题:三选一使差值最小 题目大意 给定 $N$、$K$ 和两个长度均为 $N$ 的数组 $A$、$B$,其中 $N\le22$,$K\le N$,元素在 $10^9$ 以内。有两个人,初始值均为 0。第 $i$ 天只能三选一: 给 alpha 加上 $A[i]$; 给 beta 加上 $B[i]$; 两边都不加,跳过这一天。 跳过的天数最多为...

2026-03-28 某外卖厂暑期笔试复盘

第一题:三选一使差值最小

题目大意

给定 NN、KK 和两个长度均为 NN 的数组 AA、BB,其中 N≤22N\le22,K≤NK\le N,元素在 10910^9 以内。有两个人,初始值均为 0。第 ii 天只能三选一:

  • 给 alpha 加上 A[i]A[i];
  • 给 beta 加上 B[i]B[i];
  • 两边都不加,跳过这一天。

跳过的天数最多为 KK。求 NN 天后 ∣α−β∣|\alpha-\beta| 的最小值。

我当时的想法

看到 N≤22N\le22,我先想的是状态压缩 DP。但想了一会儿就发现,只给一个状态保留当前差值最小的方案不够:后面可能需要的是当前差值更大的那条路径。

发现这个问题后,我还是没有及时换方向,继续在原来的 DP 上想办法优化暴力。最后想写一个 3223^{22} 的暴力兜底,也没写出来。

问题不在于所有 DP 都不能做,而是这个状态没有保留后面决策需要的信息。比如同一阶段有两种差值 +3+3 和 −5-5,当前看 +3+3 更好,但后面如果加上 5,反而是 −5-5 能得到 0。只存绝对值最小的那个,就把这条路径丢了。

思路分析

把每天的选择换成对差值的贡献:选 A 是 +A[i]+A[i],选 B 是 −B[i]-B[i],跳过是 0。要找的就是跳过次数不超过 KK 时,所有贡献之和离 0 最近的方案。

直接枚举有 322≈3.1×10103^{22}\approx3.1\times10^{10} 种选择,太多了。把数组拆成两半,每半最多 11 项,各自只有 311=1771473^{11}=177147 种选择,这就是折半搜索。

先枚举左右两半,记录每种方案的差值和跳过次数。右半按跳过次数分组,每组的差值排好序。

然后枚举左半方案。假设它的差值是 ss、跳过了 cc 天,就可以在右半跳过 0∼K−c0\sim K-c 天的各组里,二分找最接近 −s-s 的值。二分位置和前一个位置都检查一下,更新最小绝对值。

这里是“最多 KK 次”,所以不能只找右半恰好跳过 K−cK-c 次的那一组。枚举组数还要受右半长度限制,避免 KK 比半边长度大时越界。

时间复杂度 O(3⌈N/2⌉(K+1)log⁡3⌈N/2⌉)O(3^{\lceil N/2\rceil}(K+1)\log 3^{\lceil N/2\rceil}),空间复杂度 O(3⌈N/2⌉)O(3^{\lceil N/2\rceil})。

代码

下面两题的代码都是事后参考实现。第一题保留折半搜索写法;没有留下的考场原码不作还原。

from bisect import bisect_left


def min_diff(N, K, A, B):
    mid = N // 2
    left_items = list(zip(A[:mid], B[:mid]))
    right_items = list(zip(A[mid:], B[mid:]))

    def enum_states(items):
        states = []

        def dfs(i, s, skip):
            if i == len(items):
                states.append((s, skip))
                return
            a, b = items[i]
            dfs(i + 1, s + a, skip)          # 选 alpha
            dfs(i + 1, s - b, skip)          # 选 beta
            dfs(i + 1, s, skip + 1)          # 跳过

        dfs(0, 0, 0)
        return states

    left_states = enum_states(left_items)
    right_states = enum_states(right_items)

    # 右半按 skip 次数分组
    right_by_skip = [[] for _ in range(len(right_items) + 1)]
    for s, c in right_states:
        if c <= K:
            right_by_skip[c].append(s)
    for arr in right_by_skip:
        arr.sort()

    ans = float('inf')

    for s1, c1 in left_states:
        if c1 > K:
            continue

        remain = K - c1
        target = -s1
        # 上界要同时受 remain 和右半元素个数限制,否则 K > len(right_items) 时会越界
        upper = min(remain, len(right_items))

        for c2 in range(upper + 1):
            arr = right_by_skip[c2]
            if not arr:
                continue
            pos = bisect_left(arr, target)
            if pos < len(arr):
                ans = min(ans, abs(s1 + arr[pos]))
            if pos > 0:
                ans = min(ans, abs(s1 + arr[pos - 1]))

    return ans

第二题:端点和与区间最大值

题目大意

给定长度为 NN 的正整数数组 AA,N≤105N\le10^5,A[i]∈[1,106]A[i]\in[1,10^6]。统计有多少个区间 [L,R][L,R],L≤RL\le R,满足

A[L]+A[R]>max⁡(A[L…R])A[L]+A[R]>\max(A[L\dots R])

我当时的想法

我先写出了 O(N2)O(N^2) 暴力。之后想到固定一个点,求它的贡献,但卡在怎么把这个点和区间最大值接起来:只有确定它就是区间最大值,端点的条件才好判断。当时没走通这一步。

思路分析

先数不满足条件的区间,最后用总区间数减掉它们。

因为数组元素都大于 0,只要最大值在端点上,两个端点的和就一定比最大值大。所以不合法区间的最大值只能在内部,且两个端点之和不超过它。

对每个位置 ii,用单调栈找到:

  • 左边第一个严格大于 A[i]A[i] 的位置,记为 pip_i,不存在则为 −1-1;
  • 右边第一个大于等于 A[i]A[i] 的位置,记为 qiq_i,不存在则为 NN。

这样,归给 ii 的区间不能越过这两个位置。如果区间里有相同的最大值,就归给最右边那个:左边允许相等,右边遇到相等就截断,每个区间只会算一次。

要数的不合法端点就是

L∈[pi+1,i−1],R∈[i+1,qi−1],A[L]+A[R]≤A[i]L\in[p_i+1,i-1],\quad R\in[i+1,q_i-1],\quad A[L]+A[R]\le A[i]

枚举两侧中较短的一侧。每枚举一个端点 xx,就在另一侧查询有多少个数不超过 A[i]−A[x]A[i]-A[x]。

关键是这个查询不能每次重新切出整个长区间排序,否则单调数组等情况下仍会退化。可以预先按数组前缀建立持久化线段树:先离散化数值,第 tt 棵树记录前 tt 个数的频次。两棵前缀树相减,就能在 O(log⁡N)O(\log N) 时间内查任意下标区间里不超过某个值的数有几个。

为什么只枚举短侧就够快?把每个区间按最大值继续分成左右两部分,就是一棵最大笛卡尔树。某个位置每次被计入短侧,往上对应的子树大小至少翻倍,所以每个位置最多被枚举 O(log⁡N)O(\log N) 次。总枚举次数 O(Nlog⁡N)O(N\log N),加上每次查询,时间复杂度 O(Nlog⁡2N)O(N\log^2 N),空间复杂度 O(Nlog⁡N)O(N\log N)。

代码

#include <bits/stdc++.h>
using namespace std;

long long count_pairs(const vector<int>& a) {
    int n = (int)a.size();
    if (n == 0) return 0;

    vector<int> left(n), right(n), st;
    for (int i = 0; i < n; ++i) {
        while (!st.empty() && a[st.back()] <= a[i]) st.pop_back();
        left[i] = st.empty() ? 0 : st.back() + 1;
        st.push_back(i);
    }
    st.clear();
    for (int i = n - 1; i >= 0; --i) {
        while (!st.empty() && a[st.back()] < a[i]) st.pop_back();
        right[i] = st.empty() ? n - 1 : st.back() - 1;
        st.push_back(i);
    }

    vector<int> values = a;
    sort(values.begin(), values.end());
    values.erase(unique(values.begin(), values.end()), values.end());
    int m = (int)values.size();

    struct Node {
        int l = 0, r = 0, sum = 0;
    };
    vector<Node> tree(1);  // 0 号是空树
    tree.reserve((size_t)n * 20 + 1);
    vector<int> root(n + 1, 0);

    // 复制一条根到叶子的路径,其他节点与旧版本共用。
    function<int(int, int, int, int)> add =
        [&](int old, int l, int r, int pos) -> int {
            int u = (int)tree.size();
            tree.push_back(tree[old]);
            ++tree[u].sum;
            if (r - l == 1) return u;
            int mid = (l + r) / 2;
            if (pos < mid) {
                int child = add(tree[old].l, l, mid, pos);
                tree[u].l = child;
            } else {
                int child = add(tree[old].r, mid, r, pos);
                tree[u].r = child;
            }
            return u;
        };

    for (int i = 0; i < n; ++i) {
        int pos = lower_bound(values.begin(), values.end(), a[i]) - values.begin();
        root[i + 1] = add(root[i], 0, m, pos);
    }

    // 两个前缀版本相减,统计数值排名在 [0, k) 内的元素个数。
    function<int(int, int, int, int, int)> query =
        [&](int u, int v, int l, int r, int k) -> int {
            if (k <= l) return 0;
            if (r <= k) return tree[u].sum - tree[v].sum;
            int mid = (l + r) / 2;
            return query(tree[u].l, tree[v].l, l, mid, k)
                 + query(tree[u].r, tree[v].r, mid, r, k);
        };

    auto count_le = [&](int l, int r, int limit) -> int {
        int k = upper_bound(values.begin(), values.end(), limit) - values.begin();
        return query(root[r + 1], root[l], 0, m, k);
    };

    long long bad = 0;
    for (int i = 0; i < n; ++i) {
        int l = left[i], r = right[i];
        if (l == i || r == i) continue;
        if (i - l <= r - i) {
            for (int x = l; x < i; ++x)
                bad += count_le(i + 1, r, a[i] - a[x]);
        } else {
            for (int x = i + 1; x <= r; ++x)
                bad += count_le(l, i - 1, a[i] - a[x]);
        }
    }
    return 1LL * n * (n + 1) / 2 - bad;
}

这两题的卡点都在建模。第一题已经发现当前最优会丢信息,却还留在原来的状态上打转;第二题已经想到固定点计贡献,却没有把“这个点是区间最大值”变成明确的范围。先把这一步说清楚,后面的枚举和计数才接得上。

Written by Handwritten

Random Posts

© 2026 Zhiheng Wang @traceonsnow
Powered by theme astro-koharu · Inspired by Shoka
Loading...