普通视图

发现新文章,点击刷新页面。
今天 — 2026年5月21日首页

枚举

作者 tsreaper
2024年2月18日 12:34

解法:枚举

我们重新整理一下题意:给定两个数组 arr1arr2,问是否存在一个数 x,使得 x 同时是 arr1arr2 中某个数的前缀。求 x 的最大长度。

一个数 $t$ 共有 $\mathcal{O}(\log t)$ 个前缀,因此 arr1arr2 的前缀总数是 $\mathcal{O}(n\log X)$ 的,我们可以把所有前缀都预处理出来。

pre1 是保存了 arr1 中所有前缀的数组,pre2 是保存了 arr2 中所有前缀的数组,问题变为:是否存在一个数 x,使得 x 同时在 pre1pre2 中出现过。求 x 的最大长度。

这个问题就非常经典了,我们用哈希表保存 pre2 里的所有元素,然后枚举 pre1 里的所有元素,看它是否在哈希表中即可。

复杂度 $\mathcal{O}(n\log X)$,其中 $X = 10^8$。

参考代码(c++)

###c++

class Solution {
public:
    int longestCommonPrefix(vector<int>& arr1, vector<int>& arr2) {
        // 把 arr2 的所有前缀放入哈希表
        unordered_set<int> st;
        for (int x : arr2) for (int y = x; y; y /= 10) st.insert(y);
        int ans = 0;
        // 枚举 arr1 中的所有前缀
        for (int x : arr1) {
            int len = 0;
            for (int y = x; y; y /= 10) len++;
            for (int y = x; y; y /= 10) {
                // 查哈希表
                if (st.count(y)) ans = max(ans, len);
                len--;
            }
        }
        return ans;
    }
};
昨天 — 2026年5月20日首页

枚举

作者 tsreaper
2023年4月30日 00:13

解法:枚举

由于 AB 都是 $n$ 的排列,因此当一个元素在 AB 直到下标 $i$ 的前缀中一共出现两次时,该元素就是一个公共元素。

由于直到下标 $i$ 的前缀与直到下标 $(i - 1)$ 的前缀相比,只新增了 A[i]B[i] 这两个元素,因此对于每一个 $i$,只要检查 A[i]B[i] 是否出现了两次,就能知道公共元素增加了几个。复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    vector<int> findThePrefixCommonArray(vector<int>& A, vector<int>& B) {
        int n = A.size();
        // cnt[x] 表示元素 x 目前一共出现了几个
        int cnt[n + 1];
        memset(cnt, 0, sizeof(cnt));
        vector<int> ans;
        // now 表示目前枚举到的前缀中,一共有几个公共元素
        int now = 0;
        for (int i = 0; i < n; i++) {
            // 检查 A[i] 和 B[i] 是否出现了两次
            if (++cnt[A[i]] == 2) now++;
            if (++cnt[B[i]] == 2) now++;
            ans.push_back(now);
        }
        return ans;
    }
};
昨天以前首页

双指针

作者 tsreaper
2023年1月22日 12:19

解法:双指针

由于数组已经排好序了,我们用两个指针分别指向 nums1nums2 中的第一个数。后续哪个数较小就让哪个指针前进,直到两个指针指向的数都一样。复杂度 $\mathcal{O}(n + m)$。

参考代码(c++)

###c++

class Solution {
public:
    int getCommon(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(), m = nums2.size();
        for (int i = 0, j = 0; i < n && j < m; ) {
            if (nums1[i] == nums2[j]) return nums1[i];
            else if (nums1[i] < nums2[j]) i++;
            else j++;
        }
        return -1;
    }
};

01 BFS

作者 tsreaper
2025年7月27日 12:05

解法:01 BFS

相邻下标间的移动很好处理:将每个下标看成点,向相邻下标连权值为 $1$ 的边。

质数传送怎么办呢?将每个质数看成特殊点,值为质数的下标向对应特殊点连权值为 $1$ 的边。同时对每个下标的值进行质因数分解,从每个质因数特殊点向该下标连权值为 $0$ 的边即可。

因为边权都是 $0$ 和 $1$,可以用 01 BFS 求最短路。复杂度 $\mathcal{O}(X\log X + n\log X)$,其中 $X = 10^6$ 是权值。

参考代码(c++)

#define MAXA ((int) 1e6)
bool inited = false;
vector<int> fac[MAXA + 5];
void init() {
    if (inited) return;
    inited = true;
    // XlogX 预处理所有数的质因数
    for (int i = 2; i <= MAXA; i++) if (fac[i].empty()) for (int j = i; j <= MAXA; j += i) fac[j].push_back(i);
}

class Solution {
public:
    int minJumps(vector<int>& nums) {
        init();
        int n = nums.size();
        // 把要用到的质数都挑出来
        unordered_map<int, int> mp;
        for (int i = 0; i < n; i++) if (fac[nums[i]].size() == 1) mp[nums[i]] = 1;
        int K = 0;
        for (auto &p : mp) p.second = n + K, K++;

        // 建图
        vector<int> e[n + K], v[n + K];
        for (int i = 0; i < n; i++) {
            // 相邻下标移动
            if (i > 0) e[i].push_back(i - 1), v[i].push_back(1);
            if (i + 1 < n) e[i].push_back(i + 1), v[i].push_back(1);
            // 质数传送:值为质数的下标向特殊点连边
            if (fac[nums[i]].size() == 1) e[i].push_back(mp[nums[i]]), v[i].push_back(1);
            // 质数传送:质因数特殊点向下标连边
            for (int x : fac[nums[i]]) if (mp.count(x)) {
                int idx = mp[x];
                e[idx].push_back(i);
                v[idx].push_back(0);
            }
        }

        // 模板:01 BFS
        int dis[n + K];
        memset(dis, -1, sizeof(dis));
        typedef pair<int, int> pii;
        deque<pii> dq;
        dq.push_back({0, 0}); dis[0] = 0;
        while (!dq.empty()) {
            pii p = dq.front(); dq.pop_front();
            int sn = p.first;
            if (dis[sn] != p.second) continue;
            if (sn == n - 1) return dis[sn];

            auto update = [&](int fn, int val) {
                int d = dis[sn] + val;
                if (dis[fn] == -1 || dis[fn] > d) {
                    dis[fn] = d;
                    if (val == 0) dq.push_front({fn, d});
                    else dq.push_back({fn, d});
                }
            };

            for (int i = 0; i < e[sn].size(); i++) update(e[sn][i], v[sn][i]);
        }
        return -1;
    }
};

观察并思考 or (分类讨论 & 树状数组)

作者 tsreaper
2025年8月24日 12:10

解法 1:观察并思考

设 $f(i)$ 表示前 $i$ 个元素的最大值,$g(i)$ 表示第 $i$ 到第 $n$ 个元素的最小值。

因为往后跳只能往更小的数走,所以如果 $f(i) \le g(i + 1)$,那么前 $i$ 个数不可能到达后面的数。然后注意到每次跳跃都是双向可通行的,所以后面的数也到不了前面的数。

反之,如果 $f(i) > g(i + 1)$,那么第 $i$ 个数可以先跳到前面的最大值 $f(i)$,然后跳到后面的最小值 $g(i + 1)$,然后再跳到第 $(i + 1)$ 个数。同样由于每次跳跃都是双向可通行的,第 $(i + 1)$ 个数也可以反过来到第 $i$ 个数。

因此,每个 $f(i) \le g(i + 1)$ 的位置就把整个序列分成了很多段,每一段的答案就是当前段的最大值。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

class Solution {
public:
    vector<int> maxValue(vector<int>& nums) {
        int n = nums.size();

        // f[i]:前 i 个元素的最大值
        // g[i]:第 i 到第 n 个元素的最小值
        int f[n], g[n + 1];
        f[0] = nums[0];
        for (int i = 1; i < n; i++) f[i] = max(f[i - 1], nums[i]);
        g[n] = 1e9;
        for (int i = n - 1; i >= 0; i--) g[i] = min(g[i + 1], nums[i]);

        vector<int> ans(n);
        // now:当前段和左边所有段的最大值
        for (int i = n - 1, now = f[i]; i >= 0; i--) {
            // 分段了,更新最大值
            // f[i] 是递增的,所以前缀最大值就是当前段的最大值
            if (f[i] <= g[i + 1]) now = f[i];
            ans[i] = now;
        }
        return ans;
    }
};

解法 2:分类讨论 & 树状数组

观察不出这个性质怎么办呢?我们还可以分类讨论答案的相对位置。

对于每个数 $x$,显然它的答案 $y \ge x$。$y = x$ 的情况都不用跳,下面只考虑 $y > x$ 的情况。

假设它的答案 $y$ 在左边,因为往左跳是往更大的数走,所以直接跳过去即可。

如果它的答案 $y$ 在右边,那么我需要先跳到 $y$ 右边一个小于 $y$ 的值 $z$,才能跳到 $y$。

那是不是直接从 $x$ 跳到 $z$ 再到 $y$ 呢?不是,考虑这个例子 [3, 1, 4, 2]。我从 $1$ 是跳不到 $2$ 的,但是我可以先从 $1$ 到 $3$,再跳到 $2$,再跳到 $4$。因为往右跳是往更小的数走,所以 $x$ 越大,能跳到的 $z$ 就越多。所以先往左跳到最大值,然后再考虑往右怎么跳。

剩下的问题就是怎么求右边比 $x$ 小的数里,最大能跳到多少。用树状数组动态维护前缀 max 即可。

复杂度 $\mathcal{O}(n\log n)$。

参考代码(c++)

class Solution {
public:
    vector<int> maxValue(vector<int>& nums) {
        int n = nums.size();

        // 元素离散化
        map<int, int> mp;
        for (int x : nums) mp[x] = 1;
        int m = 0;
        for (auto &p : mp) p.second = ++m;
        for (int &x : nums) x = mp[x];
        int actual[m + 1];
        for (auto &p : mp) actual[p.second] = p.first;

        // 维护每个位置往左能跳到的最大值
        int f[n];
        f[0] = nums[0];
        for (int i = 1; i < n; i++) f[i] = max(f[i - 1], nums[i]);

        // 树状数组模板开始

        int tree[m + 1];
        memset(tree, 0, sizeof(tree));
        
        auto lb = [&](int x) { return x & (-x); };

        auto update = [&](int pos, int val) {
            for (; pos <= m; pos += lb(pos)) tree[pos] = max(tree[pos], val);
        };

        auto query = [&](int pos) {
            int ret = 0;
            for (; pos; pos -= lb(pos)) ret = max(ret, tree[pos]);
            return ret;
        };

        // 树状数组模板结束

        vector<int> ans(n);
        for (int i = n - 1; i >= 0; i--) {
            // f[i] 是直接往左跳
            // query(f[i] - 1) 是先往左跳到最大值,然后看往右能到的最佳答案
            ans[i] = max(f[i], query(f[i] - 1));
            // 更新当前数能到的最佳答案
            update(nums[i], ans[i]);
        }
        for (auto &x : ans) x = actual[x];
        return ans;
    }
};

DP & 分类讨论

作者 tsreaper
2024年7月21日 00:23

解法:DP & 分类讨论

本题难点在于讨论清楚各种情况。

容易想到设计状态 $f(j, i)$ 表示只考虑前 $j$ 列,且第 $j$ 列涂黑了前 $i$ 行的最大得分。首先注意到不会出现连续三列都不操作的情况,因为这样会导致中间的一列不参与计分,此时可以把中间的一列全涂黑,答案不会变差。因此我们只需要考虑上一个操作的列是 $(j - 1)$,$(j - 2)$ 还是 $(j - 3)$ 的情况。

以下转移方程中,$s(j, i_l, i_r)$ 表示第 $j$ 列第 $i_l$ 行到第 $i_r$ 行的分数之和。

情况 1:当上一个操作的列是 $(j - 1)$,且它涂黑了 $i'$ 行时

情况 1.1:若 $i' \le i$,需要考虑第 $(j - 1)$ 列涂黑的格子数比 $(j - 2)$ 列多还是少。注意到最优答案下,第 $(j - 1)$ 列涂黑的格子一定比 $(j - 2)$ 列多,否则在两边涂黑的格子都比中间多的情况下,中间列的操作只是在失去分数,不优。

这种情况下,本次操作将损失第 $j$ 列前 $i'$ 行的分数,但获得了第 $(j - 1)$ 列第 $(i' + 1)$ 行到第 $i$ 的分数,以及第 $(j + 1)$ 列前 $i$ 行的分数。

另外,为了维护这一列和上一列黑格子的数量关系,我们需要在 DP 状态里加一维,变成 $f(j, i, t = 0/1)$,表示只考虑前 $j$ 列,且第 $j$ 列涂黑了前 $i$ 行,且第 $j$ 列涂黑的格子数比第 $(j - 1)$ 列少($0$)还是多($1$)时的最大得分。这个情况的转移方程是

$$
f(j, i, 1) \leftarrow f(j - 1, i' \le i, 1) - s(j, 1, i') + s(j - 1, i' + 1, i) + s(j + 1, 1, i)
$$

情况 1.2:若 $i' > i$,本次操作将损失第 $j$ 列前 $i$ 行的分数,但获得了第 $(j + 1)$ 列前 $i$ 的分数。转移方程为

$$
f(j, i, 0) \leftarrow f(j - 1, i' > i, 0 \le t \le 1) - s(j, 1, i) + s(j + 1, 1, i)
$$

情况 2:当上一个操作的列是 $(j - 2)$,且它涂黑了 $i'$ 行时

情况 2.1:若 $i' \le i$,本次操作将获得第 $(j - 1)$ 列第 $(i' + 1)$ 行到第 $i$ 行的分数,以及第 $(j + 1)$ 列前 $i$ 的分数。转移方程为

$$
f(j, i, 1) \leftarrow f(j - 2, i' \le i, 0 \le t \le 1) + s(j - 1, i' + 1, i) + s(j + 1, 1, i)
$$

情况 2.2:若 $i' > i$,本次操作将获得第 $(j + 1)$ 列前 $i$ 的分数。转移方程为

$$
f(j, i, 1) \leftarrow f(j - 2, i' > i, 0 \le t \le 1) + s(j + 1, 1, i)
$$

情况 3:当上一个操作的列是 $(j - 3)$,且它涂黑了 $i'$ 行时

本次操作将获得第 $(j - 1)$ 列前 $i$ 行的分数,以及第 $(j + 1)$ 列前 $i$ 的分数。转移方程为

$$
f(j, i, 1) \leftarrow f(j - 3, i', 0 \le t \le 1) + s(j - 1, 1, i) + s(j + 1, 1, i)
$$

情况 4:这一列是第一个被操作的列

$$
f(j, i, 1) \leftarrow s(j - 1, 1, i) + s(j + 1, 1, i)
$$

所有情况取最大值即可。枚举最后操作的是哪一列,以及涂黑了几行,答案就是 $\max(f(i, j, 0/1))$。复杂度 $\mathcal{O}(n^3)$。

参考代码(c++)

###cpp

class Solution {
public:
    long long maximumScore(vector<vector<int>>& grid) {
        int n = grid.size();
        if (n == 1) return 0;

        // 前缀和预处理分数之和
        long long sm[n + 2][n + 2];
        memset(sm, 0, sizeof(sm));
        for (int j = 1; j <= n; j++) for (int i = 1; i <= n; i++) sm[j][i] = sm[j][i - 1] + grid[i - 1][j - 1];

        const long long INF = 1e18; 
        long long f[n + 1][n + 1][2];
        for (int i = 0; i <= n; i++) for (int j = 0; j <= n; j++) f[i][j][0] = f[i][j][1] = -INF;
        for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) {
            // 情况 4
            f[i][j][1] = sm[i - 1][j] + sm[i + 1][j];
            // 情况 1.1
            if (i > 1) for (int jj = 1; jj <= j; jj++) {
                long long det = (sm[i - 1][j] - sm[i - 1][jj]) + sm[i + 1][j] - sm[i][jj];
                f[i][j][1] = max(f[i][j][1], f[i - 1][jj][1] + det);
            }
            // 情况 1.2
            if (i > 1) for (int jj = j + 1; jj <= n; jj++) {
                long long det = sm[i + 1][j] - sm[i][j];
                f[i][j][0] = max({f[i][j][0], f[i - 1][jj][0] + det, f[i - 1][jj][1] + det});
            }
            // 情况 2.1
            if (i > 2) for (int jj = 1; jj <= j; jj++) {
                long long det = (sm[i - 1][j] - sm[i - 1][jj]) + sm[i + 1][j];
                f[i][j][1] = max({f[i][j][1], f[i - 2][jj][0] + det, f[i - 2][jj][1] + det});
            }
            // 情况 2.2
            if (i > 2) for (int jj = j + 1; jj <= n; jj++) {
                long long det = sm[i + 1][j];
                f[i][j][1] = max({f[i][j][1], f[i - 2][jj][0] + det, f[i - 2][jj][1] + det});
            }
            // 情况 3
            if (i > 3) for (int jj = 1; jj <= n; jj++) {
                long long det = sm[i - 1][j] + sm[i + 1][j];
                f[i][j][1] = max({f[i][j][1], f[i - 3][jj][0] + det, f[i - 3][jj][1] + det});
            }
        }

        long long ans = 0;
        for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) ans = max({ans, f[i][j][0], f[i][j][1]});
        return ans;
    }
};

二分 & 贪心 & 单调指针

作者 tsreaper
2025年2月23日 12:21

解法:二分 & 贪心 & 单调指针

最小值最大,首先尝试二分答案 $l$。注意数据范围 $k \ge 4$,因此答案至多为正方形的边长。

只考虑小等于边长的答案有什么好处呢?考虑选了一个点 $P$ 之后,会导致哪些点不可选。因为所有点都在边界上,所以我们想象从这个点出发,往两边走出去,会发现只要不走到对面那条边上,我们越往两边走,距离 $P$ 就越远。我们从原点开始,把所有点按逆时针顺序编个号,那么如果不考虑对边,选择 $P$ 只会影响包含 $P$ 的一个区间。而由于对边到 $P$ 的距离至少有一个边长,因此我们确实可以不考虑对边。

现在问题变为:在环上有 $n$ 个点,选择至少 $k$ 个点,使得相邻两点的距离至少为 $l$。这个问题在链上是很好做的,我们先选第一个点,然后每次选择最左边的可选点即可。因为每个点的影响距离是固定的,所以选择最左边的点可以给右边留下更多可选的点。

可是环上的问题怎么办呢?我们发现,环上最大的问题在于:所选的最后一个点到第一个点的距离可能不足 $l$,那我们就不知道第一个点该选哪个比较好。

不知道选哪个的时候,那就枚举吧!可是枚举第一个点选哪个,再跑一边贪心,复杂度会变成 $\mathcal{O}(n^2)$。如何才能降低复杂度呢?

这时候就可以尝试单调性了。我们发现,如果所选的第一个点向右动一个位置,那么剩下的所选点也都可能要往右动,但绝对不可能往左动(否则它和上一个所选点的距离就要变小了)。这正是我们想要的单调性。

因此我们先假设选择第一个点,然后按链上的贪心选出 $k$ 个点。如果此时 $k$ 个点都选不出来,说明链上的问题无解,而环上的问题比链上的还多一个限制,那就更误解了,直接返回 false

如果链上的问题有解,但所选的最后一个点到第一个点的距离不足 $l$,我们就得按逆时针顺序枚举第一个点。每一个点的右移可能会波及到下一个点,因此我们还要右移每个所选点,直到它到上一个所选点的距离至少为 $l$。调整完成后,再检查最后一个点到第一个点的距离。

细心的读者可能还有一个疑问:单调指针的复杂度等于每个指针最多移动的步数,那么每个指针最多移动几步呢?如果最后一个点调整之后,甚至反超了第一个点,那肯定就无解了。而第一个点的下标范围只有 $0$ 到 $(n - 1)$,说明最后一个点的下标不会超过 $2n$。因此每个指针最多移动 $2n$ 步。

因此整体复杂度 $\mathcal{O}(nk\log X)$,其中 $X = 10^9$ 是边长的值域。

参考代码(c++)

class Solution {
public:
    int maxDistance(int side, vector<vector<int>>& points, int K) {
        int n = points.size();

        // 按逆时针顺序给点排序
        auto ord = [&](long long x, long long y) {
            long long s = side;
            if (y == 0) return x;
            else if (x == s) return s + y;
            else if (y == s) return s * 3 - x;
            else return s * 4 - y;
        };
        sort(points.begin(), points.end(), [&](vector<int> &a, vector<int> &b) {
            return ord(a[0], a[1]) < ord(b[0], b[1]);
        });

        // 求第 i 个点到第 j 个点的距离
        auto dis = [&](int i, int j) {
            return abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]);
        };

        // 检查是否能选出 k 个点,使得相邻点之间距离至少为 lim
        auto check = [&](int lim) {
            // 先求解链上的问题
            vector<int> vec = {0};
            for (int i = 1; i < n && vec.size() < K; i++)
                if (dis(i, vec.back()) >= lim) vec.push_back(i);
            // 链上问题无解,环上更无解了
            if (vec.size() < K) return false;
            // 选的第一个点刚好就是对的
            if (dis(vec[0], vec.back()) >= lim) return true;
            // 枚举第一个点选哪个
            for (int i = 1; i < n && vec.back() < n * 2; i++) {
                vec[0] = i;
                // 调整每个点,使得距离符合要求
                for (int j = 1; j < K; j++) {
                    while (dis(vec[j] % n, vec[j - 1] % n) < lim) {
                        vec[j]++;
                        // 每个指针最多移动 2n 步
                        if (vec[j] >= n * 2) return false;
                    }
                }
                // 检查最后一个点到第一个点的距离
                if (vec.back() < i + n && dis(i, vec.back() % n) >= lim) return true;
            }
            return false;
        };

        // 二分答案
        int head = 1, tail = side;
        while (head < tail) {
            int mid = (head + tail + 1) >> 1;
            if (check(mid)) head = mid;
            else tail = mid - 1;
        }
        return head;
    }
};

贪心

作者 tsreaper
2023年8月27日 13:19

解法:贪心

题目稍微有点不清晰,其实问的是完成 $n$ 次移动之后的那个终点距离原点最远有多远,并不考虑经过的中间点。

终点要么尽量在左边,要么尽量在右边。所以 _ 要么都改成 L,要么都改成 R。取两种情况的最大值即可。复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    int furthestDistanceFromOrigin(string moves) {
        // 求字符串 s 的终点到原点的距离
        auto gao = [&](string s) {
            int d = 0;
            for (char c : s) {
                if (c == 'L') d--;
                else d++;
            }
            return abs(d);
        };

        // 所有 _ 都改成 L
        string L = moves;
        for (char &c : L) if (c == '_') c = 'L';
        // 所有 _ 都改成 R
        string R = moves;
        for (char &c : R) if (c == '_') c = 'R';
        // 取两种情况的最大值
        return max(gao(L), gao(R));
    }
};

前缀和

作者 tsreaper
2023年4月9日 12:28

解法:前缀和

注意到如果 nums[i]nums[j] 不同,则它们不会互相影响。因此我们可以用一个 unordered_map<int, vector<int>> 保存同一种数的所有下标,然后单独处理每种数即可。

接下来考虑某一种数的答案怎么算出来。问题其实就是:给一个有序数组,求每个元素和其它元素差值的绝对值的和。这是出现过无数次的经典问题,解法参考 leetcode 1685. 有序数组中差绝对值之和

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    vector<long long> distance(vector<int>& nums) {
        int n = nums.size();

        // 维护每种数的所有下标
        unordered_map<int, vector<int>> mp;
        for (int i = 0; i < n; i++) mp[nums[i]].push_back(i);

        vector<long long> ans(n);
        // 求某一种数的答案
        auto gao = [&](vector<int> &vec) {
            int n = vec.size();
            long long f[n + 1];
            f[0] = 0;
            for (int i = 0; i < n; i++) f[i + 1] = f[i] + vec[i];
            for (int i = 1; i <= n; i++) {
                long long L = 1LL * i * vec[i - 1] - f[i];
                long long R = (f[n] - f[i]) - 1LL * (n - i) * vec[i - 1];
                ans[vec[i - 1]] = L + R;
            }
        };

        // 对每种数求答案
        for (auto &p : mp) gao(p.second);
        return ans;
    }
};

双指针

作者 tsreaper
2025年11月30日 12:15

解法:双指针

考虑整数 x,假设我们把序列里的所有 x 变成红色,所有 reverse(x) 变成蓝色, 我们就可以枚举所有红色,看左边最近的蓝色在哪里。这一问题可以用双指针解决。

枚举序列中出现过的所有不同整数 x,取最小答案即可。复杂度 $\mathcal{O}(n)$。

参考代码(c++)

class Solution {
public:
    int minMirrorPairDistance(vector<int>& nums) {
        int n = nums.size();

        // 求 reverse(x)
        auto gao = [&](int x) {
            vector<int> vec;
            for (; x; x /= 10) vec.push_back(x % 10);
            int ret = 0;
            for (int y : vec) ret = ret * 10 + y;
            return ret;
        };

        // pos1 记录每种元素出现的所有位置
        // pos2 记录每种 reverse 出现的所有位置
        unordered_map<int, vector<int>> pos1, pos2;
        for (int i = 0; i < n; i++) {
            pos1[nums[i]].push_back(i);
            pos2[gao(nums[i])].push_back(i);
        }

        const int INF = 1e9;
        int ans = INF;
        for (auto &entry : pos1) if (pos2.count(entry.first)) {
            auto &vec1 = entry.second;
            auto &vec2 = pos2[entry.first];
            // vec1[i] 是当前枚举到的元素下标,vec2[j] 是大于等于 vec1[i] 的最近 reverse 的下标
            // 所以 vec2[j - 1] 就是小于 vec[i] 的最近 reverse 的下标
            for (int i = 0, j = 0; i < vec1.size(); i++) {
                while (j < vec2.size() && vec2[j] < vec1[i]) j++;
                if (j - 1 >= 0) ans = min(ans, vec1[i] - vec2[j - 1]);
            }
        }
        return ans < INF ? ans : -1;
    }
};

不会做怎么办

本题是双指针的简单应用,不会做本题的读者可以学习 灵神题单 - 滑动窗口与双指针 的“双序列双指针”一节。

贪心

作者 tsreaper
2025年3月16日 12:30

解法:贪心

如果不是环形序列,为了让距离差尽量小,每个元素只要考虑上一个和下一个相同元素即可。

环形序列怎么做呢?设序列长度为 $n$,我们把序列复制一遍加在原序列后面,然后按普通序列的方法计算这个长度为 $2n$ 的序列的答案。设新序列中下标为 $i$ 的元素答案为 $v_i$,则原序列中下标为 $i$ 的元素答案为 $\min(v_i, v_{i + n})$。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

class Solution {
public:
    vector<int> solveQueries(vector<int>& nums, vector<int>& queries) {
        int n = nums.size();
        // 把序列复制一遍
        for (int i = 0; i < n; i++) nums.push_back(nums[i]);

        int ans[n * 2];
        for (int i = 0; i < n * 2; i++) ans[i] = 1e9;

        // 计算每个元素距离上一个相同元素多远
        unordered_map<int, vector<int>> mp;
        for (int i = 0; i < n * 2; i++) {
            auto &vec = mp[nums[i]];
            if (!vec.empty()) ans[i] = min(ans[i], i - vec.back());
            vec.push_back(i);
        }

        // 计算每个元素距离下一个相同元素多远
        mp.clear();
        for (int i = n * 2 - 1; i >= 0; i--) {
            auto &vec = mp[nums[i]];
            if (!vec.empty()) ans[i] = min(ans[i], vec.back() - i);
            vec.push_back(i);
        }

        vector<int> ret;
        for (int x : queries) {
            int t = min(ans[x], ans[x + n]);
            if (t < n) ret.push_back(t);
            else ret.push_back(-1);
        }
        return ret;
    }
};

利用关键结论进行 DP,含证明

作者 tsreaper
2022年11月6日 12:08

解法:DP

设机器人有 $n$ 个,工厂有 $m$ 个。不失一般性地,假设机器人的坐标是递增的,工厂的坐标也是递增的。

关键结论

设最优方案中,机器人 $i$ 被送去工厂 $t_i$。注意到关键结论

存在最优方案,使得 $t_i$ 是不严格单调递增的。

证明

引理:若存在 $1 \le x < y \le n$ 满足 $t_x > t_y$,此时让机器人 $x$ 去工厂 $t_y$,机器人 $y$ 去工厂 $t_x$,答案不会变得更差。

不失一般性地,设机器人 $x$ 的坐标小等于工厂 $t_x$ 的坐标(若实际情况不是如此,将机器人 $x$ 和工厂 $t_x$ 的坐标对换,机器人 $y$ 和工厂 $t_y$ 的坐标对换,可以发现距离不变)。为了证明引理,对以下三种情况进行讨论:

image.png

相信看过我的题解的朋友会觉得这张图很眼熟。没错,这张图和第 316 场周赛的 使数组相似的最少操作次数 几乎一模一样。

DP

有了关键结论,我们就能用 DP 求最优答案。既然 $t_i$ 是不严格单调递增的,我们就对每一段相同的 $t_i$ 进行 DP。

设 $d(l, r, x)$ 表示将第 $l$ 到 $r$ 个机器人都送去工厂 $x$ 的总距离。维护 $f(i, j)$ 表示已经送走了前 $i$ 个机器人,且第 $i$ 个机器人送去工厂 $j$ 的最小总距离。转移方程为

$$
f(i, j) = \min\limits_{0 \le i' < i, 0 \le j' < j} (f(i', j') + d(i' + 1, i, j)) \quad \text{s.t. } \quad i - i' \le \text{工厂 } j \text{ 的容量}
$$

这个转移方程就是在枚举把哪一段的机器人全部送去工厂 $j$。初值 $f(0, 0) = 0$。答案就是 $\min\limits_{j=1}^m f(n, j)$。

最小的 $f(i', j')$ 用前缀 min 维护即可。令 $g(i, j) = \min\limits_{0 \le j' \le j} f(i, j')$,那么转移方程可以改为

$$
f(i, j) = \min\limits_{0 \le i' < i} (g(i', j - 1) + d(i' + 1, i, j)) \quad \text{s.t. } \quad i - i' \le \text{工厂 } j \text{ 的容量}
$$

初值 $f(0, 0) = 0$,$g(0, *) = 0$。答案就是 $g(n, m)$。复杂度 $\mathcal{O}(n\log n + m\log m + n^2m)$。

参考代码(c++)

###c++

class Solution {
public:
    long long minimumTotalDistance(vector<int>& robot, vector<vector<int>>& factory) {
        // 机器人和工厂的坐标排序
        sort(robot.begin(), robot.end());
        sort(factory.begin(), factory.end());

        const long long INF = 1e15;
        int n = robot.size(), m = factory.size();
        // g[i][j] 表示 min(f[i][j']),j' <= j
        long long f[n + 1][m + 1], g[n + 1][m + 1];
        // 初值
        for (int i = 0; i <= n; i++) for (int j = 0; j <= m; j++) f[i][j] = g[i][j] = INF;
        f[0][0] = 0;
        for (int j = 0; j <= m; j++) g[0][j] = 0;
        
        // 套转移方程
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                long long d = 0;
                for (int ii = i - 1; ii >= 0 && i - ii <= factory[j - 1][1]; ii--) {
                    d += abs(robot[ii] - factory[j - 1][0]);
                    f[i][j] = min(f[i][j], g[ii][j - 1] + d);
                }
            }
            for (int j = 1; j <= m; j++) g[i][j] = min(g[i][j - 1], f[i][j]);
        }

        return g[n][m];
    }
};

数学 & 贪心

作者 tsreaper
2025年11月9日 12:04

解法:数学 & 贪心

不妨假设 $i < j < k$,则距离之和为 $(j - i) + (k - i) + (k - j) = 2(k - i)$。

为了最小化 $2(k - i)$,$i$ 和 $k$ 的下标需要尽量接近。单独考虑每种数,从小到大枚举它出现的下标 $k$,则 $i$ 就是这种数往前两次出现的下标,才是尽量接近的。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

class Solution {
public:
    int minimumDistance(vector<int>& nums) {
        int n = nums.size();

        unordered_map<int, vector<int>> mp;
        for (int i = 0; i < n; i++) mp[nums[i]].push_back(i);

        const int INF = 1e9;
        int ans = INF;
        for (auto &p : mp) {
            auto &vec = p.second;
            int sz = vec.size();
            for (int i = 2; i < sz; i++) ans = min(ans, (vec[i] - vec[i - 2]) * 2);
        }
        return ans < INF ? ans : -1;
    }
};

根号分类讨论(sqrt trick)& 扫描线

作者 tsreaper
2025年8月18日 12:42

解法:根号分类讨论(sqrt trick)& 扫描线

如果 $k_i > \sqrt{n}$,我们直接暴力计算,因为下标每次增加 $\sqrt{n}$,最多加 $\sqrt{n}$ 次就到 $n$ 了。维护这种操作的复杂度是 $\mathcal{O}(q\sqrt{n})$。

如果 $k_i \le \sqrt{n}$,注意到本次操作被修改的下标 $\mod k_i$ 都和 $l_i \bmod k_i$ 相等,又因为只要求最后的答案,所以我们可以把这个信息先分类记下来:步长为 $k_i$,且下标 $\mod k_i$ 为特定值,且位于区间 $[l_i, r_i]$ 里的所有下标都要乘以 $v_i$。步长只有 $\sqrt{n}$ 种,每种步长的 $\bmod$ 也只有 $\sqrt{n}$ 种,因此我们只有 $\mathcal{O}(n)$ 类信息要记录。

怎么把我们记录的信息还原成答案呢?我们枚举每类信息,这样问题变为:每次给一个区间乘以 $v_i$,问每个数最后的值。这就是 leetcode 1109. 航班预定统计,对操作区间排序,使用差分 + 扫描线的思想即可离线处理。1109 题里,加法的逆运算是减法,本题里模意义下乘法的逆运算是求逆元。

还原答案的复杂度是多少呢?注意到相同步长不同余数的下标是不会重复的,因此每种步长会恰好把所有下标枚举一遍,因此我们会枚举 $\mathcal{O}(n\sqrt{n})$ 次下标,再加上对操作的排序,因此复杂度为 $\mathcal{O}(q\log q + n\sqrt{n})$。

最后考虑求逆元的复杂度,整体复杂度为 $\mathcal{O}(q(\log q + \log M) + n\sqrt{n})$,其中 $M = 10^9 + 7$ 是模数。

参考代码(c++)

class Solution {
public:
    int xorAfterQueries(vector<int>& nums, vector<vector<int>>& queries) {
        int n = nums.size(), B = sqrt(n);

        // 求乘法逆元
        const int MOD = 1e9 + 7;
        auto inv = [&](long long a) {
            long long b = MOD - 2, y = 1;
            for (; b; b >>= 1) {
                if (b & 1) y = (y * a) % MOD;
                a = a * a % MOD;
            }
            return y;
        };

        long long A[n];
        for (int i = 0; i < n; i++) A[i] = nums[i];
        typedef pair<int, long long> pil;
        vector<pil> vec[B + 1][B + 1];
        for (auto &qry : queries) {
            int l = qry[0], r = qry[1], K = qry[2], v = qry[3];
            if (K <= B) {
                // 步长不超过根号,先把操作记下来
                // 差分思想:记录操作开始的位置以及原运算,再记录操作结束的位置以及逆运算
                vec[K][l % K].push_back({l, v});
                vec[K][l % K].push_back({r + 1, inv(v)});
            } else {
                // 步长超过根号,暴力处理
                for (int i = l; i <= r; i += K) A[i] = A[i] * v % MOD;
            }
        }

        // 枚举每一类操作
        for (int k = 1; k <= B; k++) for (int m = 0; m < k; m++) {
            // 把操作按下标从左到右排序
            sort(vec[k][m].begin(), vec[k][m].end());
            // 扫描线维护当前乘积
            long long now = 1;
            // 枚举这一类里的所有下标
            for (int i = m, j = 0; i < n; i += k) {
                // 用扫描线进行需要的操作
                while (j < vec[k][m].size() && vec[k][m][j].first <= i) {
                    now = now * vec[k][m][j].second % MOD;
                    j++;
                }
                A[i] = A[i] * now % MOD;
            }
        }

        long long ans = 0;
        for (int i = 0; i < n; i++) ans ^= A[i];
        return ans;
    }
};

模拟

作者 tsreaper
2025年8月18日 12:43

解法:模拟

数据范围较小,模拟即可。复杂度 $\mathcal{O}(nq)$。

参考代码(c++)

class Solution {
public:
    int xorAfterQueries(vector<int>& nums, vector<vector<int>>& queries) {
        int n = nums.size();
        long long A[n];
        for (int i = 0; i < n; i++) A[i] = nums[i];

        const int MOD = 1e9 + 7;
        for (auto &qry : queries) for (int i = qry[0]; i <= qry[1]; i += qry[2]) A[i] = A[i] * qry[3] % MOD;
        
        long long ans = 0;
        for (int i = 0; i < n; i++) ans ^= A[i];
        return ans;
    }
};
❌
❌