题解:洛谷 P14917 数字移动

【题目来源】

洛谷:P14917 [GESP202512 五级] 数字移动 - 洛谷

【题目描述】

小 A 有一个包含 \(N\) 个正整数的序列 \(A=\{A_1,A_2,\cdots,A_N\}\),序列 \(A\) 恰好包含 \(\frac{N}{2}\) 对不同的正整数。形式化地,对于任意 \(1 \le i \le N\),存在唯一一个 \(j\) 满足 \(1\le j \le N, i\neq j, A_i=A_j\)

小 A 希望每对相同的数字在序列中相邻,为了实现这一目的,小 A 每次操作会选择任意 \(i(1\le i\le N)\),将当前序列的第 \(i\) 个数字移动到任意位置,并花费对应数字的体力。

例如,假设序列 \(A=\{1,2,1,3,2,3\}\),小 A 可以选择 \(i=2\),将 \(A_2=2\) 移动到 \(A_3=1\) 的后面,此时序列变为 \(\{1,1,2,3,2,3\}\),耗费 \(2\) 点体力。小 A 也可以选择 \(i=3\),将 \(A_3=1\) 移动到 \(A_2=2\) 的前面,此时序列变为 \(\{1,1,2,3,2,3\}\),花费 \(1\) 点体力。

小 A 可以执行任意次操作,但他希望自己每次花费的体力尽可能小。小 A 希望你能帮他计算出一个最小的 \(x\),使得他能够在每次花费的体力均不超过 \(x\) 的情况下令每对相同的数字在序列中相邻。

【输入】

第一行一个正整数 \(N\),代表序列长度,保证 \(N\) 为偶数。

第二行包含 \(N\) 个正整数 \(A_1,A_2,\ldots,A_N\),代表序列 \(A\)。且对于任意 \(1\le i\le N\),存在唯一一个 \(j\) 满足 \(1\le j\le N,i\neq j,A_i=A_j\)

数据保证小 A 至少需要执行一次操作。

【输出】

输出一行,代表满足要求的 \(x\) 的最小值。

【输入样例】

6
1 2 1 3 2 3

【输出样例】

2

【核心思想】

  1. 问题分析:给定一个长度为 \(N\) 的序列(\(N\) 为偶数),每个数字恰好出现两次。每次操作可以移动任意一个元素到任意位置,花费等于该元素的数值。目标是通过若干次移动,使每对相同数字在序列中相邻,且要求每次移动的数值均不超过 \(x\),求最小的可行 \(x\)。由于只能移动数值 \(\le x\) 的元素,而 \(> x\) 的元素位置固定,问题转化为判定:删除所有 \(\le x\) 的元素后,剩余的元素(均 \(> x\))是否已经成对相邻。因为只有数值小的元素可以被移动,大元素不能被移动,它们必须已经在原始序列中按对相邻(中间可被小元素隔开,但小元素可被移走,所以相邻的判定需忽略小元素)。

  2. 算法选择

    • 整数二分答案\(x\) 的取值范围为 \([1, \max(A_i)]\),且可行性具有单调性——若 \(x\) 可行,则任意更大的 \(x\) 也可行。因此可对 \(x\) 进行二分。
    • 贪心判定函数 check(x):遍历原数组,跳过所有 \(\le x\) 的元素,对 \(> x\) 的元素进行配对检查。维护一个变量 last 表示当前等待配对的数字。若遇到一个新的大元素,若 last == 0 则将其记录为 last;否则,若该元素与 last 相同则配对成功,置 last = 0;若不同,则说明存在无法通过移动小元素调整的大元素错位,返回 false。遍历结束后,若 last == 0 则返回 true,否则返回 false
    • 单调性保证:随着 \(x\) 增大,被允许移动的元素增多,剩余不可移动的元素减少,条件更容易满足,因此二分成立。
  3. 关键步骤

    • 读入\(N\) 以及序列 \(A[1..N]\)
    • 二分边界:左边界 \(l = 1\),右边界 \(r = \max(A_i)\)(或 \(10^5\),根据数据范围)。
    • 判定函数
      • 初始化 last = 0
      • 遍历 \(i\)\(1\)\(N\)
        • \(A_i \le x\),跳过。
        • 否则,若 last == 0,则 last = A_i
        • 否则,若 last == A_i,则 last = 0
        • 否则返回 false
      • 返回 last == 0
    • 二分过程while (l < r),取 mid = (l + r) / 2,若 check(mid) 为真,则 r = mid,否则 l = mid + 1
    • 输出:最终 \(l\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log M)\),其中 \(M = \max(A_i)\),二分约 \(\log 10^5 \approx 17\) 次,每次判定 \(O(N)\)
    • 空间复杂度:\(O(N)\),存储数组。
  5. 二分答案 + 贪心判定的核心思想

    • 移动权限与固定元素:只有数值 \(\le x\) 的元素可被移动,因此 \(> x\) 的元素相对位置不可变。为了使所有相同元素相邻,这些固定元素必须在原始序列中(忽略可移动元素后)已经成对相邻。
    • 配对检查的贪心性:从左到右扫描,用 last 记录当前未配对的固定元素,遇到相同元素则配对,否则直接失败。这是检查“序列是否由若干对相邻相同元素组成”的标准方法。
    • 单调性与二分:随着 \(x\) 增大,可移动元素增多,固定元素减少,可行性单调递增,因此二分搜索最小可行 \(x\)
    • 适用场景:适用于“只能移动部分元素,且移动代价与元素值相关”的序列调整问题,通过二分阈值和贪心判定求解最小代价上限。

【算法标签】

普及 #整数二分

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, a[N], maxn;  // n: 数组元素个数, a: 存储数组, maxn: 最大值(代码中未使用)

// 检查函数:验证是否能在限制x下使数组满足配对条件
bool check(int x)
{
    int t = 0;  // 临时变量,用于记录当前等待配对的数字

    // 遍历数组中的每个元素
    for (int i = 1; i <= n; i++)
    {
        // 如果当前元素小于等于x,可以忽略
        if (a[i] <= x) continue;

        // 处理大于x的元素
        if (!t)  // 如果t为0,表示没有等待配对的数字
            t = a[i];  // 将当前数字设为需要配对的数字
        else if (a[i] != t)  // 如果当前数字与等待配对的数字不同
            return 0;  // 无法满足条件,返回false
        else  // 如果当前数字与等待配对的数字相同
            t = 0;  // 配对成功,重置t为0
    }

    // 如果最后t为0,说明所有大于x的数字都成功配对
    return 1;
}

int main()
{
    cin >> n;  // 输入数组长度

    // 读取数组元素
    for (int i = 1; i <= n; i++)
        cin >> a[i];

    int l = 1, r = 100000;  // 二分查找的左右边界,r设为最大值100000

    // 二分查找最小的x
    while (l < r)
    {
        int mid = (l + r) / 2;  // 取中间值
        if (check(mid))  // 如果mid满足条件
            r = mid;  // 尝试更小的x,右边界缩小到mid
        else
            l = mid + 1;  // 否则需要更大的x,左边界增加到mid+1
    }

    cout << l << endl;  // 输出最小满足条件的x
    return 0;
}

【运行结果】

6
1 2 1 3 2 3
2
posted @ 2026-09-03 14:14  团爸讲算法  阅读(22)  评论(0)    收藏  举报