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 状态设成:“已经搞完了...的条件下,再去搞...的花费” 这样的。