Codeforces 1235E Antenna Coverage 做题心得

Codeforces 1235E Antenna Coverage 做题心得

这才发现我连最基本的贪心/dp都不会了,一直在LCT,SAM啥的,人脑子都没了。赶快来搞搞dp/贪心

注:本篇记录了全部的思维过程,以及对此类问题的分析。

如果您只是想看解法,跳到 “改进的dp“ 一章。

假的贪心 (可以跳过)

考虑最左边那个天线,它一定(?)要覆盖它左边的那些位置,并且比后面那些区间优。

覆盖完了之后,跳到这个天线右边去,继续贪心。

考虑这样为什么不对:确实,单看左边这些位置,最左边的天线是比后面的优;但是最后面的区间可能来覆盖一下左边的位置,顺便就把后面也覆盖完了,然后总体就更优了。

于是贪心挂了。

想了一下,数据结构也不行(至少我不会,或者复杂度不对)。考虑 dp。

也许只有我这种sb会想出来这样sb的贪心了吧

初步的dp

首先考虑一个常规的dp:设 \(f_i\) 表示,填满 \([1,i]\),最小花费是多少

然后答案就是 \(f_m\) 了。

转移也挺简单的:枚举一个位置 \(i\),枚举一个天线,把天线的右端点对齐到 \(i\)(花费一定代价扩展),然后算出此时左端点,转移。

然后这样出了点小问题:可能会做无用功。

对于 \(f_i\),我们完全可能还填了 \(i\) 后面的部分,完全无法保证。然后可能就,我觉得它还没填,还要多花点花费去点,实际上它可能已经填了。总之就是它并不能保证最小。

那咋整啊?

也许只有我这种sb想不出来怎么整吧

到这我就开始看题解了

改进的dp

我们来考虑一个不常规的dp:设 \(f_i\) 表示,我在 已经恰好填了 \([1,i]\) 区间的情况下,填满 \([i+1,m]\) 的最小花费。然后倒着转移,以保证前面 “已经填了 \([1,i]\)” 这个限制。

这个dp的优越性在于它加入了一个强制,强制填过的区间恰好是 \([1,i]\),不能有多的。这就排除了无用功的存在。这样,保证了该 dp 的正确性。

那为什么不直接设 \(f_i\) 表示恰好填 \([1,i]\) 呢?因为这样可能转移的区间会半跨过 \(i\),虽然可以用线段树维护,但是多一个 \(\log\) 时间就过不去了。

这个dp另一个好,在于它很好处理跨过 \(i\) 的情况:恰好填满 \([1,i]\) 的若干区间里肯定有一个右端点是 \(i\),把这个区间延长出去就行了 (不需要管它是谁,没必要)。这样,保证了该 dp 的复杂度。

上面提到,恰好填满 \([1,i]\) 的若干区间中一定有一个右端点是 \(i\)。一个显然的情况是直接把它拎出来填满。

于是得到了 \(f_i\) 的上限:\(m-i\)。换句话说我们可以先设置 \(f_i\) 的默认值为 \(m-i\),然后做后面的转移。

这样保证了所有的 dp 值,包括最后一个天线右边的那些位置的 dp 值 ,都有一个有效值,而不是 \(\infin\)

然后考虑如何转移。

\(f_i\) 是填一段前缀。要转移,就是要填更长的前缀,以凑出 \(f_{j} (j>i)\) 的形式。

注意到我们后面并没有限制填法,那我们后面就填一段前缀。根据题意,我们必须靠扩展天线范围来实现。很容易想到,我们枚举 \(i\) 后面的某一根天线,把它的左端点扩展到 \(i+1\) 这个位置上来。设它此时的右端点是 \(r\)。计算一下这一步的花费,然后 \(r\) 后面还需要再填。

回顾一下。 \(f_i\) 定义是填了 \([1,i]\),现在这个天线又填了 \([i+1,r]\),那一共就是填了 \([1,r]\),然后再去填后面 —— 没错,根据定义,它就是 \(f_r\)

注意考虑一个特殊情况,就是区间不用扩展也能转移的情况,即存在一个天线满足它能覆盖 \(i+1\) 这个位置。这个时候直接拿 \(f_{i+1}\) 更新 \(f_i\) 即可。

那答案是甚么呢?我们发现它总是有一个 “已经填了...” 的限制,但是我们的初始状态应该是锤子也没填的状态。

那好整,令我们填了 \([1,0]\) (一个空区间)即可。换句话说答案就是 \(f_0\)

边界?\(f_m=0\)。 (显然)

注意位置 \(i\) 的枚举要从 \(m-1\)\(0\)

代码

#include <bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
    #define N 200005
    #define F(i,l,r) for(int i=l;i<=r;++i)
    #define D(i,r,l) for(int i=r;i>=l;--i)
    #define Fs(i,l,r,c) for(int i=l;i<=r;c)
    #define Ds(i,r,l,c) for(int i=r;i>=l;c)
    #define MEM(x,a) memset(x,a,sizeof(x))
    #define FK(x) MEM(x,0)
    #define Tra(i,u) for(int i=G.st(u),v=G.to(i);~i;i=G.nx(i),v=G.to(i))
    #define p_b push_back
    #define sz(a) ((int)a.size())
    #define all(a) a.begin(),a.end()
    #define iter(a,p) (a.begin()+p)
    #define PUT(a,n) F(i,1,n) printf("%d ",a[i]); puts("");
    int I() {char c=getchar(); int x=0; int f=1; while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar(); while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return ((f==1)?x:-x);}
    template <typename T> void Rd(T& arg){arg=I();}
    template <typename T,typename...Types> void Rd(T& arg,Types&...args){arg=I(); Rd(args...);}
    void RA(int *p,int n) {F(i,1,n) *p=I(),++p;}
    int n,m;
    struct node{int x,s;} a[N];
    void Input()
    {
        Rd(n,m);
        F(i,1,n) 
        {
            a[i]=(node){I(),I()};
        }
    }
    int dp[N];
    void Sakuya()
    {
        MEM(dp,0x3f); dp[m]=0;
        D(i,m-1,0)
        {
            dp[i]=m-i; // 初始值
            F(j,1,n)
            {
                int l=a[j].x-a[j].s,r=a[j].x+a[j].s; // 这根天线覆盖的左右端点
                if (l<=i+1 and i+1<=r) // 直接转移的情况
                {
                    dp[i]=dp[i+1];
                }
                else if (l>i+1)
                {
                    int ext=l-(i+1); // 需要扩展的长度
                    dp[i]=min(dp[i],dp[min(r+ext,m)]+ext); // 注意这里的右端点要和 m 取 min,防止越界
                }
            }
        }
        printf("%d\n",dp[0]);
    }
    void IsMyWife()
    {
        Input();
        Sakuya();
    }
}
#undef int //long long
int main()
{
    Flandre_Scarlet::IsMyWife();
    getchar();
    return 0;
}

后记:如何想到这个 “改进的dp”

我也是看了别的题解才想到的

见过这一次,以后就能想到了吧

毕竟我是第一次见 dp 状态设成:“已经搞完了...的条件下,再去搞...的花费” 这样的。

posted @ 2021-03-06 22:44  Flandre-Zhu  阅读(46)  评论(0编辑  收藏  举报