反悔贪心(经典题目讲解)

反悔贪心

常规贪心思维题目可以看这篇博客:贪心算法入门详解_贪心算法巡回-CSDN博客

题目简述

场景 :数轴上有 n 只老鼠和 m 个洞,每个洞有容量(最多能进几只老鼠)。

目标 :让所有老鼠都进洞,使得总移动距离 最小。老鼠 i 进洞 j 的距离 = | xi - pj |

输入

  • 第一行:n m
  • 第二行:n 个老鼠坐标
  • 接下来 m 行:每行一个洞的坐标和容量

输出

  • 能全进洞 → 输出最小总距离
  • 所有洞容量加起来不够 n 只老鼠 → 输出 -1

数据规模n, m ≤ 10^6,坐标 ≤ 1e9,答案可能很大。

解题流程:

第一直觉:就近匹配(×

就近匹配为什么错:

可以看上面的例子 M1 离洞2最近,可是洞2满了之后,后面三只老鼠去洞1反而会更远,这里老鼠的行走路径其实是出现了交叉,你可以抽象的理解一下交叉就意味着行走的路径有重合就浪费了更多的路程,那自然就会比优解走的更多

优解(为什么)

1.最优匹配绝不交叉(排序)

刚才的示例中

假设M1去了洞2,M4去了洞1,这样两条路就像交叉的X

在优解中把M1和M4这两条路对调总距离就减少

2.合适的洞给合适的老鼠(容量约束)

从刚才的示例也可以看出对于容量有限的洞,离它最近的老鼠最适合进洞

3.反悔贪心

反悔:允许已经匹配的老鼠去更好的洞,把旧洞给别的鼠

步骤 操作 代码对应
1. 先进洞 老鼠匹配当前最优洞 d = a[i].fi - q2.top().fians += d
2. 发身价 给老鼠存一个身价 q1.push(a[i].fi + d)
3. 新洞反悔 右边新洞出现,判断换洞是否省距离 if (a[i].fi - q1.top() < 0) 执行反悔
几何判断

这里的几何推论看着复杂其实很简单,就是针对当前这只老鼠,让旧洞以它对称做一个镜像点 ,这时候去镜像点的距离就等于去旧洞的距离 ,然后这样就可以更好的和新洞去作比较看新的洞是不是离老鼠更近,距离的推算图片中也有,明白我说的之后应该很好理解,下面的代码就是对应代码中的变量,理解这些就知道代码中为什么要那么计算了

例子

还是最开始那张图的例子

比如你先遍历到洞1,然后M1出现了先去洞1,M2也去洞1,M3也去洞1,M4也去洞1,这时候接着往后遍历数轴,发现了洞2,这时候最外面的的老鼠是M4,所以让M4去试一下,具体就是上面的几何判断,发现更近就去M2,这时候M2满了就不用管了,但是如果没满呢,那你就接着用M3去算是不是去洞2更近,这里就体现出来反悔的过程,并且每一次都是最后一只老鼠就是最靠右的老鼠去反悔转移,这样也不会出现交叉的情况

代码
cpp 复制代码
#include<bits/stdc++.h>
using namespace std;

#define int long long
#define endl '\n'
#define pii pair<int,int>          // pair: first=坐标, second=容量(洞)或0(老鼠)
#define fi first
#define se second

const int N = 2e6 + 6;             // 最多1e6老鼠 + 1e6洞

/*
    a[i] 存储所有对象:
    - 如果是老鼠:a[i].fi = 坐标,a[i].se = 0
    - 如果是洞:  a[i].fi = 坐标,a[i].se = 剩余容量
*/

pii a[N];

/*
    q1: 老鼠堆(大根堆)
    存的是每只老鼠的"身价"
    身价 = 老鼠坐标 + 它去旧洞的距离 = 2*x - p
    堆顶 = 身价最大的老鼠 = 最右边的老鼠 = 最值得被踢去右边新洞的老鼠
*/
priority_queue<int> q1;

/*
    q2: 洞堆(大根堆)
    存的是候选洞 {坐标, 下标}
    堆顶 = 坐标最大的洞 = 最右边的洞
    老鼠来的时候,优先取堆顶的洞(因为扫描顺序保证这是当前最优的洞)
*/
priority_queue<pii> q2;

void slove() {
 	int n,m; cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>a[i].fi;

    int sum=0;
    for(int i=n+1;i<=n+m;i++) {
        cin>>a[i].fi>>a[i].se;
        sum += a[i].se;
    }
    if(sum < n) { 
        cout << -1;
        return;
    }

    n += m;
    sort(a+1, a+1+n);

    int ans=0;
    for(int i=1;i<=n;i++) {
        if(a[i].se) {   // 遇到洞
            // 反悔:用当前洞挖走之前的老鼠
            while(q1.size() && a[i].se && a[i].fi - q1.top() < 0) {
                int d = a[i].fi - q1.top();
                q1.pop();
                a[i].se--;
                ans += d;                    // d<0,总距离下降
                q2.push({a[i].fi + d, 0});   // 旧洞重新上架
            }
            // 剩余容量入堆
            if(a[i].se) {
                a[i].se--;
                q2.push({a[i].fi, i});
            }
        } else {        // 遇到老鼠
            int d = INT_MAX;
            if(q2.size()) {
                d = a[i].fi - q2.top().fi;
                int j = q2.top().se;
                q2.pop();
                if(a[j].se) {       // 容量续杯
                    a[j].se--;
                    q2.push({a[j].fi, j});
                }
            }
            ans += d;
            q1.push(a[i].fi + d);   // 记身价 = 坐标+距离
        }
    }
    cout << ans;
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int _ = 1;
    // cin >> _;   // 本题只有一组数据
    while (_--) slove();
    return 0;
}

这道题的反悔贪心只是借用了费用流"反向边"的思想,并没有真正跑费用流算法

费用流解决的是任意图上的最小费用匹配。它靠'反向边'来撤销之前的选择,这道题只是在数轴上有了数学上不能交叉的逻辑限制

费用流概念 反悔贪心中的对应 通俗解释
源点 → 老鼠(容量1,费用0) 排序后遇到老鼠 每只老鼠必须被分配
老鼠 → 洞(容量1,费用=距离) q2 取堆顶洞,算 d 老鼠选一个洞匹配
洞 → 汇点(容量=c,费用0) a[i].se 容量限制 洞最多装 c 只老鼠
反向边(费用= -原费用) q1 里存的老鼠令牌 x + d 允许撤销之前的匹配
增广路(找负环) a[i].fi - q1.top() < 0 发现换洞能省钱,执行反悔
残量网络 q1 + q2 两个堆 存所有可调整的"机会"
最小费用最大流 最终 ans 所有老鼠进洞的最小总距离

我们用两个堆去实现反向边和增广路

算法本质

反悔贪心 = 普通贪心 + 纠偏机制

普通贪心只看距离,可能让"左鼠占右洞",破坏顺序。反悔贪心保留一个"纠偏机制"------当右边的洞出现时,把最不该占左洞的那只老鼠踢到右边,恢复"左配左、右配右"的天然顺序。

因为最优匹配绝不交叉,所以恢复顺序 = 找到最优解

个人理解:

我认为这道题本质应该是数学几何题,而不是简单的贪心,把题目的老鼠和洞等模型抽象为具体的数轴和点,数轴是单向的,点与点之间的转移交叉着肯定不如顺着走,这在数学中被称为最优传输理论,并且有严格的数学表达,感兴趣的也可以自己去了解一下吧

其实和排序不等式那种正匹配和负匹配很像

相关推荐
_Narcissus_1 小时前
排序算法总结表格
c语言·数据结构·c++·笔记·算法·排序算法·总结
一只小小的芙厨2 小时前
前缀和与差分
数据结构·算法
船厂电气自动化ai大模型2 小时前
AI大模型与数学第42课:泰勒级数完整展开(神经网络近似核心)
人工智能·python·深度学习·算法·机器学习
从琳开始9893 小时前
优选算法——双指针(算法原理+力扣题)
算法·leetcode·职场和发展
从琳开始9893 小时前
优选算法——滑动窗口(概念+解题模板+LeetCode例题讲解)
算法·leetcode·职场和发展
Nil2083 小时前
leetcode 94二叉树的中序遍历
算法·leetcode·职场和发展
兔兔兔兔13 小时前
记录C++ 8
开发语言·c++
不会就选b3 小时前
算法日常・每日刷题
算法
一只小小的芙厨3 小时前
最短路总结
数据结构·算法