Luogu2605 [ZJOI2010]基站选址

原题链接:https://www.luogu.com.cn/problem/P2605

基站选址

题目描述

N N N 个村庄坐落在一条直线上,第 i ( i > 1 ) i(i>1) i(i>1) 个村庄距离第 1 1 1 个村庄的距离为 D i D_i Di。需要在这些村庄中建立不超过 K K K 个通讯基站,在第 i i i 个村庄建立基站的费用为 C i C_i Ci。如果在距离第 i i i 个村庄不超过 S i S_i Si 的范围内建立了一个通讯基站,那么就村庄被基站覆盖了。如果第 i i i 个村庄没有被覆盖,则需要向他们补偿,费用为 W i W_i Wi 。现在的问题是,选择基站的位置,使得总费用最小。

输入格式

输入文件的第一行包含两个整数 N , K N,K N,K,含义如上所述。

第二行包含 N − 1 N-1 N1 个整数,分别表示 D 2 , D 3 , ⋯   , D N D_2,D_3,\cdots,D_N D2,D3,,DN ,这 N − 1 N-1 N1 个数是递增的。

第三行包含 N N N 个整数,表示 C 1 , C 2 , ⋯   , C N C_1,C_2,\cdots,C_N C1,C2,,CN

第四行包含 N N N 个整数,表示 S 1 , S 2 , ⋯   , S N S_1,S_2,\cdots,S_N S1,S2,,SN

第五行包含 N N N 个整数,表示 W 1 , W 2 , ⋯   , W N W_1,W_2,\cdots,W_N W1,W2,,WN

输出格式

输出文件中仅包含一个整数,表示最小的总费用。

输入输出样例

输入 #1
3 2
1 2
2 3 2
1 1 0
10 20 30
输出 #1
4

说明/提示

40%的数据中, N ≤ 500 N \leq 500 N500

100%的数据中, K ≤ N K\leq N KN K ≤ 100 K\leq 100 K100 N ≤ 20 , 000 N\leq 20,000 N20,000 D i ≤ 1000000000 D_i \leq 1000000000 Di1000000000 C i ≤ 10000 C_i\leq 10000 Ci10000 S i ≤ 1000000000 S_i \leq1000000000 Si1000000000 W i ≤ 10000 W_i \leq 10000 Wi10000

题解

可恶,我明明是来学数据结构的啊!

首先,此题一眼望去就感觉跟数据结构没什么关系,找不出到底要维护什么。反而更像个动态规划题,不如先看看朴素的动态规划该如何处理。

d p [ i ] [ j ] dp[i][j] dp[i][j]为只考虑前 i i i个村庄,且第 j j j个基站就建在第 i i i个村庄时的最小花费,那么转移方程就是: d p [ i ] [ j ] = m i n k ∈ [ 1 , i ) ( d p [ k ] [ j − 1 ] + ∑ p ∈ ( k , i ) 且 未 被 覆 盖 w [ p ] ) + c [ i ] dp[i][j]=min_{k\in[1,i)}(dp[k][j-1]+\sum_{p\in(k,i)且未被覆盖}w[p])+c[i] dp[i][j]=mink[1,i)(dp[k][j1]+p(k,i)w[p])+c[i]

这是将第 j j j座基站建在第 i i i个村庄的情况,我们需要快速求一个最小值。假若 i i i村庄不建设基站,则对于被覆盖范围为 [ k , i ] [k,i] [k,i]的村庄(对于第 p p p个村庄,它的被覆盖范围就是离第 1 1 1个村庄的距离 ∈ [ d [ p ] − s [ p ] , d [ p ] + s [ p ] ] \in[d[p]-s[p],d[p]+s[p]] [d[p]s[p],d[p]+s[p]]的村庄编号范围)来说,之后的村庄建设基站肯定不可能再覆盖到它了,若上一个基站建设在第 k k k个村庄之前,就需要补偿,后面的 d p dp dp值如果需要从转移 k k k之前的转移,则需要加上此类村庄的补偿费用,即我们需要对区间 [ 1 , k − 1 ] [1,k-1] [1,k1]的村庄都进行一次区间加操作。

看到这里,就是一棵维护最小值并支持区间加的线段树的故事了。同时,我们需要维护每个村庄的被覆盖范围 s t [ i ] , e d [ i ] st[i],ed[i] st[i],ed[i]

建树时,初始值为 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1],之后遍历所有村庄,先按照转移方程取一次最小值更新 d p [ i ] [ j ] dp[i][j] dp[i][j],然后再找到所有 e d ed ed = = i ==i ==i的村庄 p p p,为区间 [ 1 , s t [ p ] − 1 ] [1,st[p]-1] [1,st[p]1]加上 w [ i ] w[i] w[i],就能完成一轮更新。

由于每次 d p [ i ] [ j ] dp[i][j] dp[i][j]只会从 j − 1 j-1 j1的部分求值,在建树完成后就不会再使用了,所以我们可以使用滚动数组,减少掉一维空间。

至于统计 e d ed ed值为 i i i的村庄,我使用了vector数组,也有大佬使用了链式前向星,纯凭个人喜好。求 s t , e d st,ed st,ed数组的时候如果使用lower_bound函数,则需要检查 e d ed ed是否满足 d [ e d [ i ] ] > d [ i ] + s [ i ] d[ed[i]]>d[i]+s[i] d[ed[i]]>d[i]+s[i],有则需要将 e d [ i ] − 1 ed[i]-1 ed[i]1,因为lower_bound返回的是大于等于查找值的值的下标,而我们要找的实际上是小于等于查找值的。

代码

因为 d p [ i ] dp[i] dp[i]表示在 i i i点建基站时的最优花费,且没有考虑之后的村庄,我们可以在所有村庄的最后面多加一个必定会被选中的村庄( c [ i ] = 0 c[i]=0 c[i]=0),这样直接与 d p [ n ] dp[n] dp[n] m i n min min就能得到答案。

#include<bits/stdc++.h>
#define ls v<<1
#define rs v<<1|1
using namespace std;
const int M=20005;
struct node{int le,ri,mn,delta;}tree[M<<2];
int n,k,ans,d[M],c[M],s[M],w[M],st[M],ed[M],dp[M];
vector<int>bound[M];
void up(int v){tree[v].mn=min(tree[ls].mn,tree[rs].mn);}
void build(int v,int le,int ri)
{
    tree[v].le=le,tree[v].ri=ri,tree[v].delta=0;
    if(le==ri){tree[v].mn=dp[le];return;}
    int mid=le+ri>>1;
    build(ls,le,mid);build(rs,mid+1,ri);
    up(v);
}
void push(int v,int delta){tree[v].mn+=delta,tree[v].delta+=delta;}
void down(int v){if(tree[v].delta)push(ls,tree[v].delta),push(rs,tree[v].delta),tree[v].delta=0;}
void add(int v,int le,int ri,int delta)
{
    if(le>ri)return;
    if(le<=tree[v].le&&tree[v].ri<=ri){push(v,delta);return;}
    down(v);
    if(le<=tree[ls].ri)add(ls,le,ri,delta);
    if(tree[rs].le<=ri)add(rs,le,ri,delta);
    up(v);
}
int ask(int v,int le,int ri)
{
    if(le>ri)return 0;
    if(le<=tree[v].le&&tree[v].ri<=ri){return tree[v].mn;}
    int r=INT_MAX;
    down(v);
    if(le<=tree[ls].ri)r=ask(ls,le,ri);
    if(tree[rs].le<=ri)r=min(r,ask(rs,le,ri));
    return r;
}
void in()
{
    scanf("%d%d",&n,&k);
    for(int i=2;i<=n;++i)scanf("%d",&d[i]);
    for(int i=1;i<=n;++i)scanf("%d",&c[i]);
    for(int i=1;i<=n;++i)scanf("%d",&s[i]);
    for(int i=1;i<=n;++i)scanf("%d",&w[i]);
}
void ac()
{
    ++n,++k;
    d[n]=w[n]=INT_MAX;
    for(int i=1;i<=n;++i)
    st[i]=lower_bound(d+1,d+1+n,d[i]-s[i])-d,
    ed[i]=lower_bound(d+1,d+1+n,d[i]+s[i])-d,
    ed[i]-=d[ed[i]]>d[i]+s[i],
    bound[ed[i]].push_back(i);
    for(int i=1,tmp=0,j;i<=n;++i)
    for(dp[i]=tmp+c[i],j=bound[i].size()-1;j>=0;--j)tmp+=w[bound[i][j]];
    ans=dp[n];
    for(int i=2,j,p;i<=k;++i,ans=min(ans,dp[n]))
    for(build(1,1,n),j=1;j<=n;++j)
    for(dp[j]=ask(1,1,j-1)+c[j],p=bound[j].size()-1;p>=0;--p)
    add(1,1,st[bound[j][p]]-1,w[bound[j][p]]);
    printf("%d\n",ans);
}
signed main()
{
    in(),ac();
    system("pause");
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ShadyPi

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

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

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

打赏作者

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

抵扣说明:

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

余额充值