引水入城( NOIP 2012)
题意:原题说的很明白,并不难理解,就是上面的点可以顺高度梯度扩展到下面的点。一种情况是最后一行的点无法被完全访问,此时是求最后一行的无法被访问的点的个数,另一种情况是最后一行的点可以被完全覆盖,而此时就换做求在第一行选择扩展点的最小个数。
题析:其实我并不是今天才遇到这道题,刚碰到这道题时,我犹豫了许久,但最后还是没敢去碰。但实际上,这道题虽说初次做是有一点困难(dalao勿喷!),但在搞懂了这道题的实际做法时,你又会恍然大悟,它其实并不如你想象中的那么复杂(就如一牛说言:这道题即没用高级的结构又没用高级的算法,何谈复杂)。
引入的话还是不写了,既然这道题看起来有点杂乱,那我们就要用划分子问题(?)的方法,一步步搞倒这道题。那就按题目说要求的来做,首先,我们需要考虑如何去求最后一行点的问题。而对于求一个点是否可以被访问的一系列问题,我们想到的,最简单的算法,那就是搜索,所以我们为何不可以干脆将第一行的点的水闸全部放开,让它们自由地输水,而我们只需要枚举一遍最后一行的访问情况,直接下定论就可以了,此处最多的搜索情况为500*500=25000,以BFS+判重数组即可轻松搞定,也不知是不是就是网上所说的灌水法。
还剩下一个求最小用点的问题,对于这个问题,我就不能提太多了,网上一般的解法是把第一行的每个点在最后一行能覆盖的区间(注意,此处的覆盖点为什么呈一个连续区间,需要一定证明)看做一个线段,紧接着就是线段覆盖问题中的一种,求出最小用线段数即可了。这里贴出一些链接,或许有大家在这道题中想要看到的东西:
百度文库解题报告,在最后有用图来证明区间问题:文库1
百度文库解题报告,最后用了比较常规的方式证明了区间问题,清晰易懂:文库2
此题所联系的线段覆盖问题,以及其它线段覆盖系列的问题,没看过的同学可以看一下,这一类问题还是别具特色的,这里丢了一个博客,当然也可以自己另外再发掘:线段覆盖
当然还有各类大牛的博客值得一看,在此不再一一列举。
代码:最后贴上丑陋的代码,请多各牛多多包涵~
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#define INF 0x3f3f3f3f
using namespace std;
const int N=500+5;
struct Line{ // 线段结构体
int l,r;
friend bool operator <(Line temp1,Line temp2){
if ( temp1.l!=temp2.l )return temp1.l<temp2.l;
else return temp1.r>temp2.r;
}
};
struct Point{ // 点结构体
int h,l;
};
// 行列扩展数组
int dh[5]={0,0,0,-1,1};
int dl[5]={0,-1,1,0,0};
// 题目数据
int n,m;
int het[N][N];
//
Line a[N]; // 记录第一行每个点在最后一行的覆盖区间(线段)
bool vis[N][N]; // 访问记录数组
queue<Point> q; // Bfs队列
//
void Bfs(){
while( !q.empty() ){
Point point=q.front();
q.pop(); // 取点
int h=point.h,l=point.l; // 用h,l简化表示
for(int i=1;i<=4;i++)
if ( h+dh[i]<=n && h+dh[i]>=0 && l+dl[i]<=m && l+dl[i]>=0 && het[h+dh[i]][l+dl[i]]<het[h][l] && !vis[h+dh[i]][l+dl[i]] ){
// 前四个判断是防止扩展点越界,第五个是为了满足由高到低的要求,第六个是为了防止重复访问同一个点
Point npoint;
npoint.h=h+dh[i],npoint.l=l+dl[i];
q.push(npoint);
vis[h+dh[i]][l+dl[i]]=true;
}
}
return;
}
int Judge(){ // Bfs搜索所有扩展点,看是否是能使最后行完全访问
for(int i=1;i<=m;i++){ // 入队所有第一行的点,并标记访问
Point point;
point.h=1,point.l=i;
q.push(point); // 入队
vis[1][i]=true;
}
Bfs(); // Bfs,超大灌水法,直接找到最后一行的访问可能
int cnt=0; // 记录最后一行不可能访问点的个数
for(int i=1;i<=m;i++)
if ( !vis[n][i] ) cnt++;
return cnt;
}
//
int Work(){
for(int i=1;i<=m;i++){
memset(vis,false,sizeof(vis)); // 默认所点都没访问
Point point;
point.h=1,point.l=i;
q.push(point); // 入队
vis[1][i]=true;
Bfs(); // 灌水,对第一行每个点扩展到下面的可能搜索
// 记录有访问所得到的最后一行访问的最大区间,即一条线段
Line line;
line.l=0,line.r=0; // 没有太大意义的赋值,为了之后判断而用
for(int j=1;j<=m;j++)
if ( vis[n][j] ){ // 从最后一排的第一位开始找,找到第一个可以被访问的点即是线段的起点
line.l=j; // 记录该线段的左端点
break;
}
for(int j=a[i].l;j<=m;j++)
if ( !vis[n][j+1] ){ // 从线段的左端点开始找,找到第一个不可以被访问的点之前的一个点即是线段的起点
line.r=j;
break;
}
if ( line.r==0 ) line.r==m; // 没有找到线段的左端点,那可知它一定是延长到最后的
a[i]=line;
//
}
//
sort(a+1,a+m+1); // 给线段排序,靠前的数据,使线段左端尽可能小,此外右端尽可能大
int k=1,last=0,best,ans=0; // 从网上搬下来的一段,以贪心法来解决线段覆盖全区间问题,记录第一行最少用点
while (last<m) {
best=0;
for(;a[k].l<=last+1 && k<=m;++k)
best=max(best,a[k].r);
last=best;
ans++;
}
//
return ans;
}
//
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>het[i][j]; // 读入数据
int wans=Judge(); // 记录最后一行不可能被访问点个数
if ( wans==0 ){ // 都可以被访问,即不存在不可能被访问的点
int ans=Work(); // 寻找访问最优解
cout<<"1"<<"\n"<<ans<<endl; // 输出可行答案
}
else cout<<"0"<<"\n"<<wans; // 输出不可行答案
// while(1); // 编译器版本低的同学需要的暂停大法
return 0;
}
抬头一看时间居然是2:33,果然写博客实在是给力啊。
一月已经过去~
2017.2.1