《算法竞赛进阶指南》 数据备份

你在一家 IT 公司为大型写字楼或办公楼的计算机数据做备份。

然而数据备份的工作是枯燥乏味的,因此你想设计一个系统让不同的办公楼彼此之间互相备份,而你则坐在家中尽享计算机游戏的乐趣。

已知办公楼都位于同一条街上,你决定给这些办公楼配对(两个一组)。

每一对办公楼可以通过在这两个建筑物之间铺设网络电缆使得它们可以互相备份。

然而,网络电缆的费用很高。

当地电信公司仅能为你提供 KK 条网络电缆,这意味着你仅能为 KK 对办公楼(总计 2K2K 个办公楼)安排备份。

任意一个办公楼都属于唯一的配对组(换句话说,这 2K2K 个办公楼一定是相异的)。

此外,电信公司需按网络电缆的长度(公里数)收费。

因而,你需要选择这 KK 对办公楼使得电缆的总长度尽可能短。

换句话说,你需要选择这 KK 对办公楼,使得每一对办公楼之间的距离之和(总距离)尽可能小。

下面给出一个示例,假定你有 55 个客户,其办公楼都在一条街上,如下图所示。

这 55 个办公楼分别位于距离大街起点 1km,3km,4km,6km1km,3km,4km,6km 和 12km12km 处。

电信公司仅为你提供 K=2K=2 条电缆。

上例中最好的配对方案是将第 11 个和第 22 个办公楼相连,第 33 个和第 44 个办公楼相连。

这样可按要求使用 K=2K=2 条电缆。

第 11 条电缆的长度是 3km−1km=2km3km−1km=2km,第 22 条电缆的长度是 6km−4km=2km6km−4km=2km。

这种配对方案需要总长 4km4km 的网络电缆,满足距离之和最小的要求。

输入格式

第一行输入整数 nn 和 KK,其中 nn 表示办公楼的数目,KK 表示可利用的网络电缆的数目。

接下来的 nn 行每行仅包含一个整数 ss,表示每个办公楼到大街起点处的距离。

这些整数将按照从小到大的顺序依次出现。

输出格式

输出应由一个正整数组成,给出将 2K2K 个相异的办公楼连成 KK 对所需的网络电缆的最小总长度。

数据范围

2≤n≤1000002≤n≤100000,
1≤K≤n/21≤K≤n/2,
0≤s≤10000000000≤s≤1000000000

输入样例:

5 2 
1
3
4
6
12

输出样例:

4

 

解题思路:
共两种情况
{
    1:选择一个最小值d[i], 在选择另一个与d[i]不想关的d[n];
    2: 选择一个最小值d[i], 最优解需要选择d[i - 1], 则情况如下面的证明:
}
性质: 选择了最小值d[i], 则不能选d[i - 1], d[i + 1],
否则如果选择了d[i],且需要选择d[i - 1]的话,则也必须选d[i + 1],且需要取消d[i]的选择;
证明:
如图所示若选择了最小值d[i],且最优解里面需要选择d[i - 1]假设我们不选择d[i + 1]而是选择
一个距离d[i + 1], d[i], d[i - 1]没有关系的一个点d[n],因为d[i] < d[i - 1]所以还不如选择d[i].
因此如果以及选择了一个最小值d[i]若最优解里面要选择d[i - 1]则必需选择d[i + 1];
即建立一个新的边这个边的边权为d[i - 1] + d[i + 1] - d[i];这句话下面会有解释 

 

代码如下:

#include <set>
#include <cstdio>
#include <iostream>

using namespace std;

const int N = 100010;

typedef long long LL;
typedef pair<LL, int> PLI;

LL d[N];
int n, k;
int l[N], r[N];

int main()
{
    cin >> n >> k;
    for (int i = 0; i < n; i ++ ) cin >> d[i];
    for (int i = n - 1; i ; i -- ) d[i] -= d[i - 1];//将原来与原点的距离转化为两个点之间的距离,即一段线段
    
    set<PLI> S;
    d[0] = d[n] = 1e18;//双链表的哨兵,防止越界;
    for (int i = 0; i < n; i ++ )
    {
        l[i] = i - 1;
        r[i] = i + 1;
        S.insert({d[i], i});
    }
    
    LL res = 0;
    while (k -- )
    {
        auto it = S.begin();
        LL v = it -> first;//取出当前所有线段的长度最小值
        int p = it -> second;//当前线段的编号
        int left = l[p], right = r[p];//left, right分别为当前线段的做儿子与右儿子
        
        res += v;//直接加上当前的最小值,为什么可以直接加,下面的注释可以解释
        
        S.erase(it);//删除当前节点以及左节点与右节点
        S.erase({d[left], left});
        S.erase({d[right], right});
        
        r[l[left]] = r[left], l[r[left]] = l[left];//双链表只需删除左右儿子,然后建立新的点
        r[l[right]] = r[right], l[r[right]] = l[right];
        
        d[p] = d[left] + d[right] - d[p];//这是新的点,代表的是不选择当前最小值d[p]而选择它的左右儿子.
        S.insert({d[p], p});
    }
    
    cout << res << endl;
    
    return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

啥也不会hh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值