P1668 [USACO04DEC] Cleaning Shifts S 题解
题目传送门
我的博客
前言
这道题有 \(3\) 种方法。本文将依次进行讲解。
做法 \(1\):贪心。笔者一开始的思路就是这个。
做法 \(2\):最短路。很巧妙的方法。
做法 \(3\):DP。
做法一:贪心
首先我们按照每个区间的左端点进行排序。这样方便我们后面贪心。假设 \(d\) 为当前需要覆盖的时间。我们每次要贪心的找能覆盖 \(d\) 的右端点最靠右的一个区间,统计到答案里。如果 \(d\) 不能被任一区间覆盖,则无解。
时间复杂度 \(O(N)\)。
代码
const int N=3e4+10;
int n,m,ans;
bool fl;
struct node{int l,r;
}a[N];
bool cmp(node A,node B){return A.l<B.l;//按照左端点排序,保证最优
}
signed main(){n=Read();m=Read();for(int i=1;i<=n;i++){a[i].l=Read(),a[i].r=Read();}sort(a+1,a+n+1,cmp);int d=1,rr=0;//d:当前位置,rr:最远的 rfor(int i=1;i<=n;i++){int j=i-1;rr=0;while(j<=n&&a[j].l<=d) {//贪心找右端点最靠右的点rr=max(rr,a[j].r);j++;}i=j-1; if(rr<d) break;//d 无法被覆盖到ans++;d=rr+1; if(rr>=m){printf("%lld\n",ans);return 0;}}puts("-1");return 0;
}
做法二:最短路
这个方法十分巧妙。看题解之前,笔者还在纳闷为甚标签里面有“最短路”。
思路借鉴这篇题解的思路。
我们建出如下图所示的图。

从 \(t+1\) 到 \(1\) 的这一条链,可以理解为每个时间均可以被覆盖,不是仅仅只能有且仅有一个奶牛在值班。额外的几个中转点,可以理解为在 \([l,r+1]\) 上有一个方案,如果要更新到 \(t+1\) 的话必然要走这些方案。
最终的答案即为 \(1\) 到 \(t+1\) 的最短路。
时间复杂度 \(O(n \log n)\)。
代码
const int N=3e4+10,M=1e6+10;
int n,m,ans;
struct edge{int nxt,to,w;
}e[M*2+N+M];//嗯对算清楚,别开小,实在不行直接 M<<2
int head[N+M],num_Edge=0;
void add_Edge(int from,int to,int w){e[++num_Edge].nxt=head[from];e[num_Edge].to=to;e[num_Edge].w=w;head[from]=num_Edge;
}
int dis[N+M];
bool vis[N+M];//数组!!不要开小!!
struct node{int id,w;bool operator < (const node &A)const {return w>A.w;}
};
priority_queue<node> q;
void dij(int s){//dijktra 板子for(int i=1;i<=n+m+1;i++) dis[i]=INF;//注意范围for(int i=1;i<=n+m+1;i++) vis[i]=0;dis[s]=0;q.push((node){s,0});while(!q.empty()){int u=q.top().id;q.pop();if(vis[u]) continue;vis[u]=1;for(int i=head[u];i;i=e[i].nxt){int v=e[i].to;if(dis[v]>dis[u]+e[i].w){dis[v]=dis[u]+e[i].w;q.push((node){v,dis[v]}); }}}
}
signed main(){n=Read();m=Read();for(int i=m+1;i>=2;i--){add_Edge(i,i-1,0);}for(int i=1;i<=n;i++){int l=Read(),r=Read();add_Edge(l,m+i+1,1);add_Edge(m+i+1,r+1,0);}dij(1);if(dis[m+1]>=INF) puts("-1");else printf("%lld\n",dis[m+1]);return 0;
}
做法三:线段树优化DP
考虑 \(n^3\) 的朴素DP。
...
for(int i=l;i<=r;i++){for(int j=l;j<=r;j++){dp[i][j]=0;}
}
...
for(int o=1;o<=t;o++)for(int i=1;i+o-1<=t;i++){int j=i+o-1;for(int k=i;k<j;k++)dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);}
尝试优化。可以把DP变成线性DP。
设 \(dp_i\) 表示 \([1,i]\) 内需要用的奶牛的数量。
如果一个奶牛 \(l=1\),则 \(dp_r=1\)。
否则转移 \(dp_r=\min \{dp_j\}\)。
需要注意的是,要按照 \(r\) 从小到大排序。
时间复杂度 \(O(nt)\)
我们又注意到每次需要求一个区间内的最小值,因此想到可以用线段树进行优化,让时间复杂度降 \(O(n \log t)\)。
代码
const int N=3e4+10,M=1e6+10;
int n,m;
struct node{int l,r;
}a[N];
bool cmp(node A,node B){return A.r<B.r;
}
//线段树板子,只需要维护区间最小值
struct tree{int l,r,mn;
}tr[M<<2];
#define ls (root<<1)
#define rs ((root<<1)|1)
void pushup(int root){tr[root].mn=min(tr[ls].mn,tr[rs].mn);
}
void built(int root,int l,int r){if(l==r){tr[root]=(tree){l,r,INF};return ;}tr[root]=(tree){l,r,INF};int mid=(l+r)>>1;built(ls,l,mid);built(rs,mid+1,r);pushup(root);
}
void change(int root,int pos,int v){//单点修改if(tr[root].l==pos&&tr[root].r==pos){tr[root].mn=v;return ;}int mid=(tr[root].l+tr[root].r)>>1;if(pos<=mid) change(ls,pos,v);else change(rs,pos,v);pushup(root);
}
int query(int root,int from,int to){//区间查询if(from<=tr[root].l&&to>=tr[root].r){return tr[root].mn;}int mid=(tr[root].l+tr[root].r)>>1;int res=INF;if(from<=mid) res=min(res,query(ls,from,to));if(to>mid) res=min(res,query(rs,from,to));return res;
}
//以上线段树
signed main(){n=Read();m=Read();built(1,1,m); for(int i=1;i<=n;i++){int l=Read(),r=Read();a[i]=(node){l,r};if(l==1) change(1,r,1);//初状态}sort(a+1,a+n+1,cmp);//按照右端点排序for(int i=1;i<=n;i++){int t=min(query(1,a[i].r,a[i].r),query(1,a[i].l-1,a[i].r-1)+1);change(1,a[i].r,t);}int t=query(1,m,m);if(t>=INF) puts("-1");else printf("%lld\n",t);return 0;
}
警示后人
- 数组一定要算清楚开多少,不要开小!!!