【二分查找&贪心】 ◆HihoCoder 1053◆ 居民迁移

针对大规模数据集上的居民迁移问题,采用二分查找与贪心算法结合的方式进行优化求解。首先,对输入数据进行排序预处理,接着利用二分查找缩小搜索范围,最后通过贪心策略验证解的可行性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

◆HihoCoder 1053◆

居民迁移


□题目□

↓HihoCoder 的链接↓


□解析□

·从头讲起

从样例看来输入的顺序并没有按位置排序,这极其不利于之后的计算,所以将居民点存储为一个结构体,然后按照位置从小到大sort一遍。但是因为是多组数据,就必须要给每一个数组清零,避免产生影响。

·二分查找

由于数据规模极大(1e9)而且不方便直接计算,所以很容易想到运用二分查找的方法来辅助求解,而二分查找的目标就是所得的答案。

·贪心检测

我们从左到右对每一个居民点进行检测,即从1~n。

这里就存在一个很简单的贪心思想——把当前城市的人,无论是否超过二分出的限制(以下统称x),尽可能的移动到人数未超过x的能够到达的位置最靠前的点。这句话比较绕,其实就是下面这样:

解析1

但是如果当前点能够到达的点都无法继续移动(也就是能到达范围内的点的人数都大于等于x了),而且当前点人数仍然大于x,就说明当前方案不可行。

它的证明比较简单——假设当遍历到点 i 的时候,它无法访问到点 i-1 能够访问到的最靠前的点 f 了,但是点 f 的人数小于x且点 i 能够访问到点 i-1, 则点 i 能够访问到最近的点 g 也一定能够访问到点 f ,所以若点 g 不将点 f 填满,则点 g 的人数一定会更多,也就是点 i 能够移动到点 g 的人数一定更少,同理,点 i 能够转移的人数就会变少,可能会超出x,则不符合最优解。就像下面:

这里写图片描述

由于一个人不能移动两次,则需要存两个值——tot[i][0] 表示点i有多少人可以移动(原住民);tot[i][1] 表示点i有多少人不能移动(搬迁后)。

为了节省时间,我们可以存两个指针——i:当前准备移动点i的人;full:现在能够到达的点中没有满的最靠前的点(其实通过之前的贪心策略,我们可以知道——full之后的点一定是没有满的)。当full或者i已经遍历完整个数组了,则判断所有的tot[][0],如果还存在一个 tot[][0] 不为0,则返回错误(因为tot[][1]一定是满的)。


□代码片□

看可以,但是别copy啊……(⊙_⊙)

/*Lucky_Glass*/
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define MAXN 100000
int n,r;
struct CITY
{
    int pos,tot;
}c[MAXN+5];
bool cmp(CITY A,CITY B){return A.pos<B.pos;}
int tot[MAXN+5][2],full;
bool Check(int x)
{
    memset(tot,0,sizeof tot);
    full=1;
    int i=1;
    tot[1][0]=c[1].tot;
    while(i<=n && full<=n)
        if(abs(c[i].pos-c[full].pos)>r)
            if(full>i) return false;
            else full++;
        else
            if(tot[full][1]+tot[i][0]<=x)
                tot[full][1]+=tot[i][0],tot[i][0]=0,i++,tot[i][0]=c[i].tot;
            else
                tot[i][0]-=x-tot[full][1],tot[full][1]=x,full++;
    for(int j=1;j<=n;j++)
        if(tot[j][0])
            return false;
    return true;
}
int Solve(int l,int r)
{
    while(l+1<r)
    {
        int mid=(l+r)/2;
        if(Check(mid))
            r=mid;
        else
            l=mid;
    }
    return r;
}
int main()
{
    int T;scanf("%d",&T);
    while(T--)
    {
        int MAX=0;
        scanf("%d%d",&n,&r);
        for(int i=1;i<=n;i++)
            scanf("%d%d",&c[i].pos,&c[i].tot),MAX=max(MAX,c[i].tot);
        sort(c+1,c+1+n,cmp);
        int res=Solve(0,MAX);
        printf("%d\n",res);
        for(int i=1;i<=n;i++)
            c[i].pos=c[i].tot=0;
    }
    return 0;
}
这个问题中使用 **二分查找 + 贪心验证** 的方法,是一个非常经典且高效的策略。下面我将详细解释这个思路的**整个过程**,并用图示和代码片段帮助你理解。 --- ## 🧠 思路概述 我们要解决的问题是: &gt; 给定一个数组 `nums` 和一个整数 `k`,将其划分为 `k` 个连续子数组,使得所有子数组中**最大值最小**。 我们无法直接枚举所有划分方式(因为组合太多),所以采用如下策略: ### ✅ 步骤一:确定答案范围 - 最小可能的最大子数组和至少是数组中的最大值(因为每个子数组至少包含一个元素)。 - 最大可能是整个数组的总和(即只划分成一个子数组)。 所以我们可以在 `[max(nums), sum(nums)]` 这个范围内进行**二分搜索**。 --- ### ✅ 步骤二:二分搜索猜测&ldquo;最大子数组和&rdquo; 我们设: - `left = max(nums)` - `right = sum(nums)` 然后每次取中间值 `mid = (left + right) / 2`,问一个问题: &gt; 是否可以将数组划分成不超过 `k` 个子数组,使得每个子数组的和都不超过 `mid`? 如果可以,说明我们还可以尝试更小的 `mid`;否则,需要增大 `mid`。 --- ### ✅ 步骤三:贪心判断是否可行 这是关键步骤。我们从左到右遍历数组,尽可能多地把元素加到当前子数组中,直到加上下一个元素就会超过 `mid`,此时新开一个子数组。 统计一共用了多少个子数组,如果数量 &le; k,说明这个 `mid` 是可行的。 --- ## 🔍 图解示例 以数组 `[2, 4, 7, 3, 5]`, `k = 3` 为例: 我们想找到最小的 `max_sum`,使得可以分成最多 3 个子数组,每个子数组的和 &le; `max_sum`。 初始范围:`left = 7`, `right = 21` 我们尝试中间值 `mid = 14` &rarr; 判断是否能划分成 &le; 3 个子数组,每个子数组和 &le; 14 遍历数组: - 当前和为 2+4+7=13 &rarr; 不超 - 加上 3 &rarr; 16 &gt; 14 &rarr; 新开一个子数组 - 然后加 5 &rarr; 3+5=8 &le; 14 总共用了 2 个子数组,小于等于 3 &rarr; 可行 于是我们可以继续尝试更小的值,比如 `mid = 10`、`mid = 8`... 最终我们会找到最小的那个满足条件的 `mid`,也就是答案。 --- ## 💻 代码逻辑(C++/Python) 下面是核心的贪心判断函数的伪代码: ```cpp bool isPossible(vector&lt;long long&gt;&amp; nums, int k, long long limit) { int count = 1; long long currentSum = 0; for (long long num : nums) { if (currentSum + num &gt; limit) { count++; currentSum = num; // 开始新的子数组 if (count &gt; k) return false; } else { currentSum += num; } } return count &lt;= k; } ``` --- ## 📈 时间复杂度分析 - 每次贪心判断是 O(n) - 二分查找次数是 log(Sum - Max) - 所以总体时间复杂度是: $$ O(n \cdot \log(\text{sum(nums)} - \text{max(nums)})) $$ 对于 `n = 2e5` 来说,这种算法是可以接受的。 --- ##
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值