反悔贪心 ——— 【AcWing.147】数据备份

文章讨论了一种涉及最小化城市电线连接总距离的问题,指出初看贪心策略(每次选择最短线段)可能不最优。通过举例说明,解释了为何需要在选择过程中允许“反悔”,即在发现更好选择时更新已有决策。提出了使用优先队列来维护备选线段,并在每次选择后更新线段组合,以达到全局最优。解决方案包括处理每个城市对之间的线段长度,使用链表和标记删除的元素,以及动态调整线段组合。
摘要由CSDN通过智能技术生成

题目

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

输入样例:

5 2 
1
3
4
6
12

输出样例:

4

题目分析

初步思路

这道题一看,第一反应就是贪心,因为要总距离最小,所以肯定要每一段都尽量的小
换句话说,因为 k < n 2 k < \frac{n}{2} k<2n 所以每一个最优解中的线段的两个端点一定是相邻的城市

A:

每次选其中最小的一段会不会影响其余的段(因为一个楼只能做为端点一次),使得全局答案不能为最优呢?

Q:

肯定的,举个例子
在这里插入图片描述
第一次最小肯定选 S 2 S_2 S2 ,然后第二次就只能选 S 1 + S 2 + S 3 S_1+S_2+S_3 S1+S2+S3 所以总计 S 2 + ( S 1 + S 2 + S 3 ) S_2 + (S_1 + S_2+S_3) S2+(S1+S2+S3)
但是换一种方法呢?
第一次选 S 1 S1 S1 ,然后第二次就可以选 S 3 S_3 S3,总计 S 1 + S 3 S_1 + S_3 S1+S3 明显更优。


那么问题就来了,这道题是不是不能贪心的求解了呢?

正确思路

肯定不是的,古人云:知错能改,善莫大焉,那么如果我们再选择的时候已经知道自己错了,我们用已知的更好的答案修改以前的答案,最后的得到的就是更好的答案

先分析一下,还是刚才那幅图
在这里插入图片描述
如果我们选择 S 2 S_2 S2 那么就不能选 S 1 , S 3 S_1,S_3 S1,S3
如果我们选择 S 1 , S 3 S_1,S_3 S1,S3 那么就不能选 S 2 S_2 S2
所以综上我们有两种方法,全局的所有方法中取最好的 K K K中即可


好了,改错的思路有了,方法也很重要
首先因为我们知道的是每个点的坐标,所以先要处理出每条选段的长度
设 S e g [ i ] = P [ i + 1 ] − P [ i ] ( 一种 P 是输入的楼房坐标 ) 设 Seg[i] = P[i + 1] - P[i] (一种P是输入的楼房坐标) Seg[i]=P[i+1]P[i](一种P是输入的楼房坐标)
然后我们要给程序反悔的机会
如果先选择了 S e g [ i ] Seg[i] Seg[i] 然后发现不好,因该选 S e g [ i − 1 ] + S e g [ i + 1 ] Seg[i - 1] + Seg[i + 1] Seg[i1]+Seg[i+1]则么办呢?
显然,减去 S e g [ i ] Seg[i] Seg[i] 再加上 S e g [ i − 1 ] + S e g [ i + 1 ] Seg[i - 1] + Seg[i + 1] Seg[i1]+Seg[i+1]即可

所以我们再选择 S e g [ i ] Seg[i] Seg[i]的同时,把 S e g [ i − 1 ] + S e g [ i + 1 ] − S e g [ i ] Seg[i - 1] + Seg[i + 1] - Seg[i] Seg[i1]+Seg[i+1]Seg[i]也加入备选的答案中不就可以了吗?这个显然可以用堆维护


那么最终的思路也就有了:
首先处理 S e g [ i ] = P [ i + 1 ] − P [ i ] Seg[i] = P[i + 1] - P[i] Seg[i]=P[i+1]P[i]
然后依次把 S e g [ i ] Seg[i] Seg[i] 放入答案
每次选取备选答案中最小的计入答案
最后删除 S e g [ i − 1 ] , S e g [ i ] , S e g [ i + 1 ] Seg[i - 1],Seg[i],Seg[i + 1] Seg[i1],Seg[i],Seg[i+1]三个点(因为暂时都不能选了)
S e g [ i − 1 ] + S e g [ i + 1 ] − S e g [ i ] Seg[i - 1] + Seg[i + 1] - Seg[i] Seg[i1]+Seg[i+1]Seg[i]插入 i i i号位置(加入备选答案)

因为每次 i − 1 i - 1 i1 i + 1 i + 1 i+1不一定是原数组中相邻的两个,所以我们可以用链表维护,然后开一个 d e l del del 数组标记被删除就好了

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5, Inf = 1e9;
int n, k, pre[N + 5], nxt[N + 5];
long long ans,pos[N + 5], seg[N + 5];
bool del[N + 5];
struct cmp {
	bool operator()(int x,int y) {
		return seg[x] > seg[y];
	}
}; // 一个很牛逼的技巧
priority_queue<int,vector<int>,cmp> q;
int main() {
	scanf("%d%d", &n, &k);
	for (int i = 1; i <= n; i++)
		scanf("%d", &pos[i]);
	for (int i = 1; i < n; i++)
		seg[i] = pos[i + 1] - pos[i];
	n --;
	seg[0] = Inf;
	for (int i = 1; i <= n; i++) {
		q.push(i);
		pre[i] = i - 1;
		nxt[i] = i + 1 > n ? 0 : i + 1;
	}
	while(k --) {
		int tp = q.top(); q.pop();
		while(del[tp]) {
			tp = q.top();
			q.pop();
		}
		ans += seg[tp];
		del[pre[tp]] = 1;
		del[nxt[tp]] = 1;
		seg[tp] = seg[pre[tp]] + seg[nxt[tp]] - seg[tp];
		q.push(tp);
		pre[tp] = pre[pre[tp]];
		nxt[tp] = nxt[nxt[tp]];
		nxt[pre[tp]] = tp;
		pre[nxt[tp]] = tp;
	}
	printf("%lld",ans);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值