关于zoj2451的题解的思考

参考链接:点击这里
下面是我的个人理解,如有错误请大神批评指正!
1.题目重点:
1.Maximizer has N inputs numbered from 1 to N.
2. Each sorter has N inputs and N outputs.Sorter(i, j) sorts values on inputs i, i+1, …, j in non-decreasing order and lets the other inputs pass through unchanged.
3.What is the length of the shortest subsequence of the given sequence of sorters in the pipeline still producing correct results for all possible combinations of input values?

2.题目重点分析
第一点说明了每个输入器是有编号的,编号为1到N,说明了输入器的顺序性。

第二点要好好地理解:
in non-decreasing order:在第一点的基础上说以不减的顺序对输入单元的值排序,假设A区间是1到k,我们对A区间的输入排好序后Maximizer不再接受在对A区间内的某个小区间的排序。
Sorter(i, j) :题目中i是第i个输入器的编号,j是第j个输入器的(i<j,下面分别记为输入器i和输入器j)。
lets the other inputs pass through unchanged:这说明了区间范围外的输入器的值是不变的,这意味着如果有输入没被排序器覆盖到最终的输出结果不一定可靠。

第三点是让我们编程求解用尽可能少的排序器就可以完成比较。
综上,题目的意思是让我们编程,找到能够最少的排序器数,用尽可能少的排序器全部输入器。

3.输入输出示例解释
由题中不减的顺序排序可知,题目提供的排序器数据可以按下图1分成以下两部分:
第一部分,蓝线框数据因为不能覆盖全部输入,所以得到的结果不可靠。
第二部分,红线框数据可以覆盖全部的输入器,所以可以得到可靠的结果。对于红线框内的第四组数据和第二、第三组数据有重叠部分,15在10和20之间,25在20和30之间,故可以去除,最终用四个排序器就可以覆盖所有输入器,得到可靠的输出结果。
图1
图1

4.解题思路
这道题是找尽量少的排序器使其能覆盖全部的输入器,题目又给了不减的顺序,所以可以采用动态规划的思想。具体的,可以从第一个输入器开始,统计覆盖第一个输入器到某个输入器的最少的排序器数量。这样,统计覆盖从输入器1到编号较大的输入器的最少排序器数量时,就可以在前面的基础上统计。后面为了说明方便,线段和排序器的含义相同。

4.1 数据存储:线段树
在这里插入图片描述
图2

4.1.1 存储结构
数组存储,只存储各节点蓝色矩形的值。因为线段树节点的左右端点比较有规律,可以写函数,用递归的方式将节点左右端点的值作为函数参数,从而可以操作特定的节点。

4.1.2 存储结构讲解
对于这个问题采用线段树来存储数据比较有优势(原因后面会提到,这部分讲的都是规则,不用问为什么)。先讲一下数据怎么存储。以N=5为例,图2是线段树的存储方式,每个节点存储三个值。红色矩形存的是区间左端点,白色矩形存的是区间右端点;对于蓝色矩形的值,分两种情况理解,如果节点不是叶子节点:我们对插入线段按其区间左端点从小到大排序,并按排序结果编号(从0开始编号,每加一条线段,编号加一,一开始会默认区间左端点为1的线段已经插入),那么蓝色矩形存的是该区间出现的第一条线段的编号。如果节点是叶子节点,假设其表示的区间为[k,k],那么蓝色矩形的值表示覆盖区间[1,k]上的最少线段数。注意,不是所有节点的蓝色矩形的值都是有效的,因为每插入一条线段,就会对含有线段右端点的区间做更新。对于不含右端点的区间是没有更新的。

4.1.3 线段编号的初始化
有存储结构可知,凡是左端点为1的区间,区间出现的第一条线段一定是编号为0的。因此我们可以先将他们初始化为0。其他区间由于一开始不能确定有多少条线段能覆盖全部输入器,故蓝色矩形的值初始化为INF,根据解题过程更新。

4.1.4 题目分析
回到我们的题目,我们想知道最少用多少条线段就能覆盖所有的输入器,那么我们需要找到区间[N,N]蓝色矩形的值。

4.2 实现过程
建树-节点值初始化-排序器插入-更新-查询[N,N]区间蓝色矩形存储的值

4.2.1 实现细节说明:

1 建树及节点初始化
由于线段树除了叶子节点外每个节点都有两个孩子,我们可以用先序遍历递归建树。递归的终止条件是区间的左右端点相同,递归函数的要将每个节点蓝色矩形的值初始为INF(一个很大的数)。对于左端点为1的区间要初始化为0,可以借助更新函数,通过线段树节点下标的规律找到区间左端点为1的节点对其更新。实现如下:

(1)函数

//建立点线段树,并将信息域设置为inf 
void BuildTree(int i,int left,int right){
    
    
	tree[i]=inf;  //起初所有节点置为+∞ 
	if(left==right) return ;  //点线段树return的状态
	
	int mid=(left+right)>>1;
	BuildTree(i<<1,left,mid);
	BuildTree(i<<1|1,mid+1,right);
}
 
//实时更新线段树,以确保下次查询时各节点的tree[i]是最优(小)值 
void Update(int i,int pos,int val,int left,int right){
    
      //i是线段树的节点序号,pos是已排序区间的端点值,val是表示到达pos所需最少的sorter数量 
	if(tree[i]>val) 
		tree[i]=val;  //若新的val比旧的tree[i]中的值小,则将其更新
	if(left==right) return ;  //点线段树return的状态
	
	int mid=(left+right)>>1;  //向下维护 
	if(pos<=mid) 
	    //如果区间偏右了,用这个来调整
		Update(i<<1,pos,val,left,mid);
	else
	    //如果区间偏左了,用这个来调整         
		Update(i<<1|1,pos,val,mid+1,right);
}

(2)函数调用表达式

BuildTree(1,1,n);
Update(1,1,0,1,n);  

2 排序器插入和数值更新
插入一个新的排序器,需要统计新排序器区间内的各叶子节点蓝色矩形框的值,看看哪个点的蓝色矩形的值最小,就返回这个最小的值,将最小值加1,可以得知覆盖当前的输入器最少需要多少个排序器。
Q:为什么要找最小的值?
A:因为,每个点的覆盖情况有3种,一种是没覆盖,蓝色矩形的值是INF,一种是有覆盖但是不是某个排序器区间的端点,此时该点蓝色矩形的值也是INF,还有一种情况是该点是某个排序器的端点,该点的蓝色矩形是一个小于INF的值,因此我们只有找到最后一种情况的点才能够得知新排序器区间排线段覆盖的情况)。做到这一点,我们首先要找到新排序器左右端点的范围,并在该范围找到最小值。
返回最小的值后,并拿这个最小的值加1去更新右端点的值即可。因为排序器是以不减的顺序进入,相对而言,右端点可能被其他线段覆盖的可能性更小,更新右端点对后面的排序器更有参考价值。

 一个很妙的想法来了!

为了使查询更高效,我们也不必去访问每个点,可以找新排序器区间内的小区间,如果经过这个小区间的第一条线段的编号(假设为t)不是INF,那么把这个值返回再+1。

Q:为什么要对返回的区间的蓝色矩形的值+1?
A:因为在设计算法的时候,计算经过长度大于1的每个区间的第一条线段的编号时已经假设把将插入的新排序器加进去了,且编号从0开始,所以在更新时要+1。至于最后是不是真的加进去就看更新函数,如果返回值+1后,比原来要大,就不会更新,因为这种情况要么是线段被其他线段完全重叠,要么是该线段和其他线段没什么交集,可能无效。
在这里插入图片描述
图3
这一点也体现了用线段树的优势,每个节点包含一个区间,从而可以通过一个区间,来统计区间内叶子点的情况,不用访问每一个节点。

实现如下:

//实时更新线段树,以确保下次查询时各节点的tree[i]是最优(小)值 
void Update(int i,int pos,int val,int left,int right){
    
      //i是线段树的节点序号,pos是已排序区间的端点值,val是表示到达pos所需最少的sorter数量 
	if(tree[i]>val) 
		tree[i]=val;  //若新的val比旧的tree[i]中的值小,则将其更新
	if(left==right) return ;  //点线段树return的状态
	
	int mid=(left+right)>>1;  //向下维护 
	if(pos<=mid) 
		Update(i<<1,pos,val,left,mid);
	else         
		Update(i<<1|1,pos,val,mid+1,right);
}
 
//查询新输入的x~y区间中可以连接新sorter的最优(小)值并返回 
int Inquiry(int i,int x,int y,int left,int right){
    
      //x~y是新的sorter区间,left~right是线段树的跨度区间 
	if(x<=left&&y>=right) return tree[i];  //若x~y覆盖left~right区间,则返回tree[i]
	
	int ans1=inf,ans2=inf;
	int mid=(left+right)>>1;
	if(x<=mid)  
		ans1=Inquiry(i<<1,x,y,left,mid);
	if(y>mid)   
		ans2=Inquiry(i<<1|1,x,y,mid+1,right);
	return ans1<ans2 ? ans1:ans2;
}  

(2)函数调用

while(m--){
    
    
			int x,y;
			scanf("%d%d",&x,&y);
			int temp=Inquiry(1,x,y,1,n);   
			Update(1,y,++temp,1,n);  //加入新的sorter,temp需要+1
		}

4.2.2 全部代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
 
const int inf=0x3f3f3f3f;
int tree[200521];
 
//建立点线段树,并将信息域设置为inf 
void BuildTree(int i,int left,int right){
    
    
	tree[i]=inf;  //起初所有节点置为+∞ 
	if(left==right) return ;  //点线段树return的状态
	
	int mid=(left+right)>>1;
	BuildTree(i<<1,left,mid);
	BuildTree(i<<1|1,mid+1,right);
}
 
//实时更新线段树,以确保下次查询时各节点的tree[i]是最优(小)值 
void Update(int i,int pos,int val,int left,int right){
    
      //i是线段树的节点序号,pos是已排序区间的端点值,val是表示到达pos所需最少的sorter数量 
	if(tree[i]>val) 
		tree[i]=val;  //若新的val比旧的tree[i]中的值小,则将其更新
	if(left==right) return ;  //点线段树return的状态
	
	int mid=(left+right)>>1;  //向下维护 
	if(pos<=mid) 
		Update(i<<1,pos,val,left,mid);
	else         
		Update(i<<1|1,pos,val,mid+1,right);
}
 
//查询新输入的x~y区间中可以连接新sorter的最优(小)值并返回 
int Inquiry(int i,int x,int y,int left,int right){
    
      //x~y是新的sorter区间,left~right是线段树的跨度区间 
	if(x<=left&&y>=right) return tree[i];  //若x~y覆盖left~right区间,则返回tree[i]
	
	int ans1=inf,ans2=inf;
	int mid=(left+right)>>1;
	if(x<=mid)  
		ans1=Inquiry(i<<1,x,y,left,mid);
	if(y>mid)   
		ans2=Inquiry(i<<1|1,x,y,mid+1,right);
	return ans1<ans2 ? ans1:ans2;
}  
 
 
//主函数 
int main(){
    
    
	int n,m;
	while(~scanf("%d%d",&n,&m)){
    
     
		BuildTree(1,1,n);
		Update(1,1,0,1,n);  //将所有线段树种中为区间端点值为1的tree[i]值更新为0
		
		while(m--){
    
    
			int x,y;
			scanf("%d%d",&x,&y);
			int temp=Inquiry(1,x,y,1,n);   
			Update(1,y,++temp,1,n);  //加入新的sorter,temp需要+1
		}
		
		printf("%d\n",Inquiry(1,n,n,1,n));	
	}
	
	return 0;
}

心得与总结:
这道题不会做的时候参考了上面链接的代码,一开始也不太看得懂,后来硬着头皮看了一天多,了解了每条语句的含义。这道题可以用动态规划的思想来解题,但是在访问每一个点(输入器)时,如果一个个去问是很耗时的,如果一个区间的点的覆盖情况相同,我们直接去访问这个区间,这样就能提高效率。而线段树可以帮忙做到这一点,因此,在代码中线段树起到了优化作用。上面是我对题目的解读,水平有限,可能有错误,也欢迎大家批评指正,谢谢各位!

猜你喜欢

转载自blog.csdn.net/ChaoyingL/article/details/118059009