反悔型贪心好例题——CF1271D Portals

Sweetlemon

2019-12-18 09:57:44

Solution

### CF1271D Portals 这是一道有非常多做法的题目,值得一做。 #### 基础贪心思想 一个比较重要的贪心思想是,对于每一个城堡,我们都尽可能在最晚的时间控制,也就是早控制不如晚控制。这一点很容易理解,因为如果早控制,士兵到了城堡也是闲着,不如留在军队里凑数,到了时间再前往目标城堡。 因此,我们可以记录每一个城堡的最晚控制时间,在最晚控制时间才派出士兵控制。 #### 动态规划算法 看到这题的数据范围居然只有 $5000$,那就一定可以动态规划了。 设 $f[i][j]$ 表示攻下了前 $i$ 个城堡且手上还剩 $j$ 个士兵时,被控制的最大总价值。那么这个问题就类似一个背包,直接转移一下就可以得到答案了。只要注意边界——打第 $i$ 个城堡前手上必须有至少 $a[i]$ 个士兵。 时间复杂度 $O(n^2)$ 量级。 ```cpp #include <cstdio> #include <cctype> #include <vector> #include <functional> #include <algorithm> #define MAXIOLG 25 #define FILENAME(x)\ freopen(x".in","r",stdin);\ freopen(x".out","w",stdout); #define LOWBIT(x) ((x)&(-(x))) #define MAXN 5005 #define INF 19260817 using namespace std; typedef long double ld; typedef long long ll; typedef ll io_t; io_t shin[MAXIOLG]; io_t seto(void); //快读,实现略去 void ayano(io_t x,char spliter='\n'); //快写,实现略去 int n; int a[MAXN],b[MAXN],c[MAXN]; int f[MAXN][MAXN]; int tcont[MAXN]; //每个城堡控制的最晚时间 pair<int,int> prs[MAXN]; //把可控制的城堡和它的价值放在 pair 中 int main(void){ int m,k; n=seto(),m=seto(),k=seto(); for (int i=1;i<=n;i++) a[i]=seto(),b[i]=seto(),c[i]=seto(),tcont[i]=i; while (m--){ int u,v; u=seto(),v=seto(); tcont[v]=max(tcont[v],u); } for (int i=1;i<=n;i++) prs[i]=make_pair(tcont[i],c[i]); sort(prs+1,prs+n+1); for (int t=0;t<=k;t++) f[0][t]=0; for (int t=k+1;t<=5000;t++) f[0][t]=-INF; int tpoint=1; //当前可控制的城堡指针 for (int i=1;i<=n;i++){ for (int t=0;t<=5000;t++) f[i][t]=-INF; for (int t=a[i];t<=5000;t++) f[i][t+b[i]]=max(f[i][t+b[i]],f[i-1][t]); while (tpoint<=n && prs[tpoint].first==i){ for (int t=0;t<5000;t++) f[i][t]=max(f[i][t],f[i][t+1]+prs[tpoint].second); tpoint++; } } int ans=-1; for (int t=0;t<=5000;t++) ans=max(ans,f[n][t]); ayano(ans); return 0; } ``` #### 数据结构优化贪心算法 我们可以想到一个这样的贪心:先不控制任何一个城堡,完成游戏后再按照价值从大到小的顺序尽可能地控制城堡。 这个贪心为什么是正确的呢?主要原因还是,控制任何一个城堡需要的士兵都是 $1$ 人。如果留着这个价值较大的城堡不控制,对后续的影响最多也就是“能多控制一个价值较小的城堡”,这一定是不划算的。 那么如何判断“能否控制这个城堡”呢? 考虑控制这个城堡对后续的影响——控制了这个城堡,今后攻打城堡时可用的士兵就会少一人。因此我们可以用一个数组记录“攻打这个城堡时,可用士兵比必需士兵($a[i]$)多多少”。假设我们想要在 $t$ 时刻控制一个城堡,那么 $t$ 时刻以后的可用士兵就会少一人,我们就要检查,$t$ 时刻以后的“盈余士兵”是否都大于 $0$。如果确实大于 $0$,表示我们可以控制这个城堡,我们就需要更新“盈余士兵”——$t$ 时刻以后的盈余士兵都少一人。 区间减和区间最小值,可以方便地用线段树实现。 时间复杂度 $O(n\log n)$ 量级。 ```cpp #include <cstdio> #include <cctype> #include <vector> #include <functional> #include <algorithm> #define MAXIOLG 25 #define FILENAME(x)\ freopen(x".in","r",stdin);\ freopen(x".out","w",stdout); #define LOWBIT(x) ((x)&(-(x))) #define MAXN 5005 #define MAX4N 20005 #define INF 19260817 using namespace std; typedef long double ld; typedef long long ll; typedef ll io_t; io_t shin[MAXIOLG]; io_t seto(void); void ayano(io_t x,char spliter='\n'); struct Ene{ int l,r,val,addv; #define L(x) ((stree[(x)]).l) #define R(x) ((stree[(x)]).r) #define X(x) ((stree[(x)]).val) #define ADD(x) ((stree[(x)]).addv) #define LC(x) (((x)<<1)) #define RC(x) (((x)<<1)^1) }; int n; int a[MAXN],b[MAXN],c[MAXN]; int rst[MAXN]; int tcont[MAXN]; pair<int,int> prs[MAXN]; Ene stree[MAX4N]; void build_tree(int root,int l,int r); void add(int root,int l,int r,int v); int query(int root,int l,int r); void pushdown(int root); int main(void){ int m,k; n=seto(),m=seto(),k=seto(); for (int i=1;i<=n;i++) a[i]=seto(),b[i]=seto(),c[i]=seto(),tcont[i]=i; while (m--){ int u,v; u=seto(),v=seto(); tcont[v]=max(tcont[v],u); } for (int i=1;i<=n;i++) prs[i]=make_pair(c[i],tcont[i]); sort(prs+1,prs+n+1); for (int i=1;i<=n;i++){ if (k<a[i]){ ayano(-1); return 0; } rst[i-1]=k-a[i]; k+=b[i]; } rst[n]=k; int troot=1; build_tree(troot,1,n); ll ans=0; for (int i=n;i;i--){ int ucont=prs[i].second; int tw=prs[i].first; if (query(troot,ucont,n)>0) add(troot,ucont,n,-1),ans+=tw; } ayano(ans); return 0; } void build_tree(int root,int l,int r){ L(root)=l,R(root)=r,ADD(root)=0; if (l==r){ X(root)=rst[l]; return; } int mid=(L(root)+R(root))>>1; build_tree(LC(root),l,mid); build_tree(RC(root),mid+1,r); X(root)=min(X(LC(root)),X(RC(root))); } void add(int root,int l,int r,int v){ if (l<=L(root) && r>=R(root)){ X(root)+=v,ADD(root)+=v; return; } pushdown(root); int mid=(L(root)+R(root))>>1; (l<=mid)?(add(LC(root),l,r,v)):(void()); (r> mid)?(add(RC(root),l,r,v)):(void()); X(root)=min(X(LC(root)),X(RC(root))); } int query(int root,int l,int r){ if (l<=L(root) && r>=R(root)) return X(root); pushdown(root); int mid=(L(root)+R(root))>>1; int ans=INF; (l<=mid)?(ans=min(ans,query(LC(root),l,r))):(0); (r> mid)?(ans=min(ans,query(RC(root),l,r))):(0); return ans; } void pushdown(int root){ int tadd=ADD(root); ADD(root)=0; if ((!tadd) || (L(root)==R(root))) return; ADD(LC(root))+=tadd,X(LC(root))+=tadd; ADD(RC(root))+=tadd,X(RC(root))+=tadd; } //略去读入输出优化的实现 ``` #### 反悔型贪心 事实上,这题还有非常容易实现的反悔型贪心做法。 反悔型贪心同样基于“控制任何一个城堡需要的士兵都是 $1$ 人”的条件。它的思路是:在每个城堡的“最晚控制点”,先贪心地把城堡控制了。 这样就会存在问题:后面士兵可能就不够了,游戏失败了啊。那我们就会后悔,“要是那时没有控制某一个城堡就好了”;而程序可以实现“时光倒流”,把当时的操作反悔掉,也就是减少 $c[i]$ 个重要点数,增加一名士兵。贪心地想,因为每一个城堡都是贡献 $1$ 名士兵,所以我们肯定会选择反悔“最不重要”的、也就是 $c[i]$ 最小的城堡。 于是,我们只需要在控制城堡的时候,把城堡放进一个堆里,每次需要反悔时取堆中最不重要的城堡进行反悔即可;如果需要反悔时堆已经为空,就说明这个游戏不可能完成,输出 $-1$ 即可。 注意一个易错点:游戏结束时,如果士兵人数是负值,自然是不可行的。因此最后如果人数为负,则还要进行反悔。 时间复杂度 $O(n\log n)$ 量级。 这道题体现了反悔型贪心的基本思路(之一),非常适合作为反悔贪心的入门例题。 类似的反悔型贪心还有这道题:[SP348 EXPEDI - Expedition](https://www.luogu.com.cn/problem/SP348),可以作为例题的配套练习。它们有一个共同特点,那就是都有一个维度是 $1$——每个城堡所需士兵都是 $1$ 人;每个加油站对答案的贡献都是 $1$——这也许是这类反悔型贪心成立的条件。 下面是本题代码。 奇怪的是,dp 的代码只用时 $109 \text{ms}$,线段树的代码用时 $156 \text{ms}$,堆的代码用时竟达到 $171 \text{ms}$!理论上快的代码实际上反而慢,确实有些奇怪。 ```cpp #include <cstdio> #include <cctype> #include <functional> #include <algorithm> #define MAXIOLG 25 #define FILENAME(x)\ freopen(x".in","r",stdin);\ freopen(x".out","w",stdout); #define LOWBIT(x) ((x)&(-(x))) #define MAXN 5005 using namespace std; typedef long double ld; typedef long long ll; typedef ll io_t; io_t shin[MAXIOLG]; io_t seto(void); void ayano(io_t x,char spliter='\n'); int n; int a[MAXN],b[MAXN],c[MAXN]; int rst[MAXN]; int tcont[MAXN]; pair<int,int> prs[MAXN]; int que[MAXN]; int main(void){ int m,k; n=seto(),m=seto(),k=seto(); for (int i=1;i<=n;i++) a[i]=seto(),b[i]=seto(),c[i]=seto(),tcont[i]=i; while (m--){ int u,v; u=seto(),v=seto(); tcont[v]=max(tcont[v],u); } for (int i=1;i<=n;i++) prs[i]=make_pair(tcont[i],c[i]); sort(prs+1,prs+n+1); int qtail=0,tpoint=1; ll ans=0; for (int i=1;i<=n;i++){ while (k<a[i] && qtail){ k++; ans+=que[0]; pop_heap(que,que+qtail); qtail--; } if (k<a[i]){ ayano(-1); return 0; } k+=b[i]; while (tpoint<=n && prs[tpoint].first==i){ k--,ans+=prs[tpoint].second; que[qtail++]=-prs[tpoint].second; push_heap(que,que+qtail); tpoint++; } } while (k<0 && qtail){ k++; ans+=que[0]; pop_heap(que,que+qtail); qtail--; } ayano(ans); return 0; } //略去读入输出优化的实现 ```