【马蹄集】第八周作业

第八周作业






MT2016 数据流的中位数

难度:黄金  时间限制:1秒  占用内存:128M
题目描述

对于数据流问题,小码哥需要设计一个在线系统,这个系统不断的接受一些数据,并维护这些数据的一些信息。

中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。

请帮小码哥设计一个支持以下两种操作的系统:

  • + num 从数据流中添加一个整数 k 到系统中 ( 0 < k < 2 32 0<k<2^{32} 0<k<232 )。

  • ? 返回目前所有元素的中位数。

格式

输入格式:第一行输入一个整形 n ( n ≤ 100000 ) n(n≤100000) n(n100000)

     第二行输入 n n n 个操作数。

输出格式:? 询问时输出中位数,每个操作一行,保证每次查询都合法。

样例 1

输入:5

   + 1

   + 2

   ?

   + 3

   ?

输出:1.5


相关知识点:模拟


题解


题目的要求很简单,有一个会提两类询问的系统:一个是增加数据,另一个是输出现有数列的中位数。我们的任务是对系统的所有指令给予执行。这是一道很明显的模拟题。

基于此,我们可以定义一个数组。每当系统要求增加数据时,可采取 选择排序 的方法来执行插入,以维持数组的有序性;每当系统要求输出中位数时,又能根据当前数组的长度来找到其中位数。这是最简单直接的办法,但是其必定超时。因为对于一次插入数据请求而言:选择插入位置和调整数组中的元素位置的时间复杂度均为 O ( n ) O(n) O(n) ,所以对于 n n n 次插入数据请求而言,其时间复杂度将达到 O ( n 2 ) O(n^2) O(n2)。而题目的插入操作次数最大能取到 10 0000 ,因此这必然会超时。


方法一:利用多重集合

从上面的分析可知,求解该题最核心的地方在于 如何加速动态插值过程中的排序过程 ,这不难让我们想到利用一些具有快速排序功能的容器。

set(集合)是按一定次序存储元素的容器(默认升序),其中的元素总是按其内部比较对象(类型比较)所指示的特定准则进行排序。其底层数据结构是 红黑树 ,因此其单次查询和插入元素(以构建有序容器)的时间复杂度均为 O ( l o g 2 n ) O(log_2n) O(log2n)。(注:set 为维护这种有序性,限定了其中的元素不能修改,只能删除再重新插入)。所以对 set 而言,在面对 n n n 次插入数据请求时(同时保证容器有序),其时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。这是能接受的。但 set 有一个问题是,其包含的元素必须唯一。而题目插入的数据却并不一定满足此条件,因此我们不得不想一想其他的数据结构。

于是,multiset(多重集合)进入了我们的视野。multiset 在实现和功能上与 set 几乎一样,其不同点主要有:

  • multiset 中的元素可以重复,而 set 中的元素必须唯一;
  • multiset 在底层存储的是 <value, value> 键值对,而 set 存储的就是 value;

注意到 multiset 中的元素是可以重复的,而这正好符合我们的需求。

但是集合 set 和多重集合 multiset 作为关联容器都有一个问题:无法通过下标索引访问元素,必须用对应的迭代器或指针。由于 multiset 无法用索引访问元素,我们在取中位数时就必须另想办法(这里肯定不能直接迭代查询,因为迭代查询的时间复杂度为 O ( n ) O(n) O(n) ,则对 n n n 次查询的时间复杂度就达到了 O ( n 2 ) O(n^2) O(n2))。

下面给出一种 “指针固定” 的方法,来使得两个指针(左指针 l 和右指针 r)在当前容器个数为奇数时,都指向最中间的数;当前容器个数为偶数时,其分别指向最中间的两个数。下面的流程每次都接受一个输入数据:

  1. 获取当前容器内的元素个数 size;

  2. 容器插入当前输入数据 num;

  3. 若 size=0,将左右指针指向容器的顶部元素;

  4. 若 size 为奇数(则说明当前左指针 l 和右指针 r 指向同一个位置,且在插入 num 后 size 将变为偶数),接下来判断 num 与左指针 l (也可以选右指针 r)指向的数据的大小(以决定 num 的插入位置):

    • 若 num<*l ,则说明当前容器在插入 num (并排序)后,num 处于左指针 l 指向的数据之前,故此时需要将左指针 l 前移一个单位。
    • 否则说明当前容器在插入 num (并排序)后,num 处于左指针 l (和右指针 r)指向的数据后,故此时需要将右指针 r 后移一个单位。
  5. 若 size 为偶数(则说明当前左指针 l 和右指针 r 指向不同位置,且在插入 num 后 size 将变为奇数),接下来需要判断 num 与左指针 l 和右指针 r 指向的数据的大小(以决定 num 的插入位置):

    • 若 *l < num < *r ,则说明当前容器在插入 num (并排序)后,num 处于左、右指针之间,此时 num 即为当前(有序)容器的中位数。故需要将左指针 l 后移一个单位、右指针 r 前移一个单位(此时左、右指针实际上指向了同一地址)。
    • 若 num<=*l ,则说明当前容器在插入 num (并排序)后,num 处于右指针 r 指向的数据之前,此时右指针 r 指向数据的前一位即为当前(有序)容器的中位数。故需要将右指针 r 前移一个单位(此时其指向的数据一定是中位数);对于左指针 l ,由于 num 在等于 *l 时,无法确定该指针指向的是哪一个,此时直接令 l=r 即可。
    • 否则说明当前容器在插入 num (并排序)后,num 处于左指针 l 指向的数据之后,此时左指针 l 指向数据的前一位即为当前(有序)容器的中位数。故需要将左指针 l 后移一个单位(此时其指向的数据一定是中位数);对于右指针 r ,由于 num 在等于 *r 时,无法确定该指针指向的是哪一个,此时直接令 r=l 即可。

通过以上步骤,就能保证每次插入数据时,左指针 l 和右指针 r 都指向有序数组的中位数:数组长度为奇数时 l=r=midPosition;为偶数时 l 和 r 分别指向最中间的两个数 。同时,也能得到取该中位数值的语句为:(*l + *r) / 2.0

基于以上思路,可写出求解该题的完整的代码为(已 AC):

/*
	数据流的中位数 
	 
	方法一:利用多重集合
*/

#include<bits/stdc++.h> 
using namespace std;

// 定义多重集合 
multiset<long> s;

// 定义左指针和右指针(初始指向end) 
multiset<long>::iterator l = s.end(), r = s.end();
 
// 定义插值函数 
void addNum(long num) {
    
    
	// 获取当前多重集合的长度 
    int size = s.size();
    
    // 将数据插入多重集合 
    s.insert(num);
    
    // 插入第一个元素
    if(size == 0) l = r = s.begin();
    
	// 若 size 为奇数(则在插入元素后,size 将变为偶数,即两个指针会指向不同地址) 
    else if(size & 1){
    
    
    	
    	// 若插入的数比左指针指向的数据还小,则左指针前移 
        if(num < *l) l--;
        
        // 否则右指针前移 
        else r++;
    }
    
    // 若 size 为偶数(则在插入元素后,size 将变为奇数,即两个指针会指向同一地址)
    else{
    
    
    	
    	// 若插入的数值处于左、右指针指向的数据之间,则该位置就是中位数(左指针后移,右指针前移) 
        if(num > *l && num < *r){
    
    
            l++;
            r--;
        }
        
        // 若插入的数值不大于左指针指向的数据,则右指针前移,并将左指针指向右指针指向的位置 
        else if(num <= *l){
    
    
            r--;
            l = r;
        }
        
        // 否则表示插入的数值不小于右指针指向的数据,则左指针后移,并将右指针指向左指针指向的位置 
        else{
    
    
            l++;
            r = l;
        }
    }
}


// 获取中位数 
float getMedian() {
    
    
    return (*l + *r) * 0.5;
}
 

int main() {
    
    
	// 录入数据 
	char opt;
	long tmp;
    int n;cin >> n;
    
    while(n--) {
    
    
        cin>>opt;
        
        // 基于 opt 分别予以答复 
        switch(opt){
    
    
        	case '+':
        		cin>>tmp;
        		addNum(tmp);
        		break;
        		
        	case '?':
        		cout<<getMedian()<<endl;
        		break;
		}
    }
 
    return 0;
}

方法二:利用堆

假设存在数列 A = { a i , i = 1 , 2 , … } A = \{ a_i , i=1,2,… \} A={ ai,i=1,2,} ,仔细思考中位数的性质(为便于理解,假设数列中的元素个数为奇数):若存在中位数 a m i d a_{mid} amid 那么对任意 i < m i d i<mid i<mid 都有 a i ≤ a m i d a_i≤a_{mid} aiamid;同理,对任意 m i d < j mid<j mid<j 都有 a m i d ≤ a j a_{mid}≤a_j amidaj 。也就是说,如果将数列 A A A 按中位数进行划分,分别得到 A 1 A_1 A1 A 2 A_2 A2 两个数列,则中位数一定满足:

a m i d ≥ m a x ( A 1 ) a m i d ≤ m i n ( A 2 ) a_{mid} ≥ max(A_1) \\ a_{mid} ≤ min(A_2) amidmax(A1)amidmin(A2)

基于这一性质,我们可以维护两个长度差异不超过 1 的堆(要求堆 A 中的所有数都小于堆 B 中的数)。接下来,将堆 A 设计为大根堆,堆 B 设计为小根堆。这样,在输出中位数时只需要比较两个堆的长度:

  • 长度相等,就说明当前系统中的数字个数为偶数,则此时的中位数为中间两个数之和再除以 2。即:从两个堆中分别取出堆顶元素,求和再除以 2。
  • 长度不等,就说明当前系统中的数字个数为奇数,则此时就看谁的长度更长?更长的那个堆的堆顶元素即为当前系统中的中位数。

那要如何保证前面提到的要求:堆 A 中的所有数都小于堆 B 中的数?

很简单,每次往堆里插入数据 num 时都提前判断一下 num 与当前系统中位数 Nmid 的大小关系,从而决定将其插入到哪个堆中。但有一点需要注意,我们必须维护两个堆的长度差异不超过 1!因此,具体的控制流程如下:

  • 若当前堆 A 的长度等于堆 B 的长度(即当前系统中的数据个数为偶数),则进一步判断:如果 num < Nmid,则将 num 插入堆 A;否则插入堆 B。
  • 若当前堆 A 的长度小于堆 B 的长度,则说明当前系统的中位数在堆 B 里。此时比较 num 与堆 B 的堆顶元素(即中位数)大小:
    • 若 num<=Nmid,则将 num 插入堆 A;
    • 否则从堆 B 中取出堆顶元素,并将该堆顶元素插入堆 A 中,然后再将 num 插入堆 B。
  • 若当前堆 A 的长度大于堆 B 的长度,则说明当前系统的中位数在堆 A 里。此时比较 num 与堆 A 的堆顶元素(即中位数)大小:
    • 若 num>=Nmid,则将 num 插入堆 B;
    • 否则从堆 A 中取出堆顶元素,并将该堆顶元素插入堆 B 中,然后再将 num 插入堆 A。

如此,就能始终保证 “堆 A 中的所有数都小于堆 B 中的数”。

基于以上思路,可写出求解该题的完整的代码为(已 AC):

/*
	数据流的中位数 
	 
	 方法二:利用堆
*/


#include<bits/stdc++.h> 
using namespace std;

// 定义两个优先队列:其中 A 队列中的所有元素取值都小于 B 
// 队列 A 为大根堆 
priority_queue<long, vector<long>, less<long> > A; 
 
// 队列 B 为小根堆 
priority_queue<long, vector<long>, greater<long> > B; 


// 获取中位数 
float getMedian() {
    
    
	// 队列 A、B  长度相等,则取各堆顶元素求和再除以 2 
    if(A.size() == B.size()) return (A.top()+B.top()) * 0.5;
	
	// 长度不等,则返回堆长度更长的那一个
	return (A.size() > B.size())?A.top():B.top(); 
}

// 定义插值函数 
void addNum(long num) {
    
    
    // 若两个队列的长度相等,则进一步比较 num 与中位数的大小关系 
    if(A.size() == B.size()){
    
    
    	
    	// 初始情况下默认插入第一个堆 
    	if(A.size() == 0){
    
    
    		A.push(num);
    		return;
		}
    	
    	// 判断大小 
    	if(num < getMedian()) A.push(num);
    	else B.push(num);
    }
	
	// 若堆 A 的长度小于堆 B 的长度,则进一步比较 num 与堆 B 的堆顶元素(即中位数)大小 
	else if(A.size() < B.size()){
    
    
		if(num <= B.top()) A.push(num);
		else{
    
    
			A.push(B.top());
			B.pop();
			B.push(num);
		}
	} 
	
	// 若堆 A 的长度大于堆 B 的长度,则进一步比较 num 与堆 A 的堆顶元素(即中位数)大小 
	else{
    
    
		if(num >= A.top()) B.push(num);
		else{
    
    
			B.push(A.top());
			A.pop();
			A.push(num);
		}
	} 
}


 
int main() {
    
    
	// 录入数据 
	char opt;
	long tmp;
    int n;cin >> n;
    
    while(n--) {
    
    
        cin>>opt;
        
        // 基于 opt 分别予以答复 
        switch(opt){
    
    
        	case '+':
        		cin>>tmp;
        		addNum(tmp);
        		break;
        		
        	case '?':
        		cout<<getMedian()<<endl;
        		break;
		}
    }
 
    return 0;
}


MT2017 连续的串

难度:钻石  时间限制:1秒  占用内存:128M
题目描述

给你一个字符串,找出现次数最多的长度为 2 的子串。

格式

输入格式:第一行为一个正整数 n ,表示给出字符串的长度,第二行为字符串。

输出格式:输出所求的串,若有多个结果,输出字典序最小的。

样例 1

输入:7

   ABACABA

输出:AB

备注

其中: 2 ≤ n ≤ 100 2≤n≤100 2n100;字符串中只包含大写字母。


相关知识点:模拟


题解


本题是一道非常基础的模拟题,其数据范围也很小,因此求解思路就是暴力法遍历求解(利用 map 构建字符串到出现次数的映射)。本题需要注意一点,在构建 map 对象时,字符串不能用 char * 型。因为 map 在构建 key-value 时,若你的字符串用 char * ,则不论你建立多少不同的 key ,最终 map 里都只有一个 key,那就是你最后创建的那个(char * 最后一次指向的地址)。

另一个细节是关于数据结构的选取。由于本题要求 “若有多个结果,输出字典序最小的” ,则其实际上暗示了你所用的数据结构要对字符串有序。因此不能单纯地想着去追求速度而选用 unordered_map

下面直接给出求解本题的完整代码

/*
	连续的串 
	 
*/


#include<bits/stdc++.h> 
using namespace std;

const int LIMIT = 2;
map<string, int> m;

void getMaxRepeat(string str){
    
    
	// 定义临时字符串容器 
	string tmp;
	
	// 统计不同子串的个数(且子串要按字典序排序) 
	for(int i=0; i<str.length()-LIMIT+1; i++){
    
    
		// 取长度为 LIMIT 的子串并存进 tmp 中 
		tmp = str.substr(i,LIMIT);

		// 为此键值的出现次数 +1 
		m[tmp]++;
	}
	
	// 定义临时最长出现次数 
	int maxRepeat = 0;
	
	// 遍历 map 寻找所有子串出现次数中的最大值 
	for(auto iter : m){
    
    
		if(iter.second > maxRepeat){
    
    
			maxRepeat = iter.second;
			tmp = iter.first;
		}
	}
	
	cout<<tmp<<endl;
}

int main( )
{
    
    
	// 获取输入 
	int n; cin>>n;
	string str; cin>>str;
	
	// 输出操作次数
	getMaxRepeat(str);
	 
    return 0;
}


MT2027 一秒成零

难度:白银  时间限制:1秒  占用内存:128M
题目描述

给定一个正整数 n n n ,请写一个函数 S t e p s Steps Steps ,将 n n n 通过以下操作变成 0,并且返回操作次数。

n n n 为偶数,则 n = n / 2 n=n/2 n=n/2

n n n 为偶数,则 n = n − 1 n=n-1 n=n1

格式

输入格式:第一行输入正整数 n 。

输出格式:输出操作次数。

样例 1

输入:14

输出:6

备注

其中: 1 ≤ n ≤ 1 0 7 1≤n≤10^7 1n107


相关知识点:模拟


题解


这道题没啥好说的,就纯模拟完事儿:

要么递推要么递推,但是鉴于数据范围,我还是觉得递推比较合适些。


/*
	一秒成零
	
*/


#include<bits/stdc++.h> 
using namespace std;

// 递推实现(数的取值范围较大时) 
int Steps(int n){
    
    
	
	// 定义操作次数 
	int optCnt = 0;
	
	// 开始递推 
	while(n){
    
    
		
		// 当前为奇数时:
		if(n&1) n--;
		
		// 否则为偶数时:
		else n/=2;
		
		// 操作次数 +1 
		optCnt++;
	}
	return optCnt;
} 

// 递归实现 
int Steps_(int n){
    
    
	
	// 终止条件
	if(n==0) return 0; 
	
	// 当前为奇数时: 
	if(n&1) return Steps(n-1)+1;
	
	// 否则为偶数时: 
	return Steps(n/2)+1; 
} 

int main( )
{
    
    
	// 获取输入 
	int n; cin>>n;
	
	// 输出操作次数
	cout<<Steps(n);
	 
    return 0;
}


MT2033 碰碰车

难度:钻石  时间限制:1秒  占用内存:128M
题目描述

游乐园玩碰碰车,其中有一种碰碰车是在一条直线上行驶。该碰碰车有一个初始朝向和初始位置,并且以每秒一个单位的速度向初始方向前行,当它和其它碰碰车相遇时,它们会立刻掉头(由于速度很快,掉头时间可以忽略)。你可以算出它们在t秒后的位置吗?

格式

输入格式:第一行有两个数字 n ( 1 − 100 ) , t ( 0 − 100 ) n (1-100),t (0-100) n(1100),t(0100),分别表示:碰碰车的数量和所用时间;接下来 n n n 行,每一行都有两个数字: x , f x,f x,f 分别表示第 n n n 辆碰碰车的位置和方向 ( 0 ≤ x ≤ 1 0 5 0≤x≤10^5 0x105,-1表示向左,1表示向右)。

输出格式:输出 n n n 行,每一行两个数字,分别表示碰碰车最后的位置和方向(-1表示向左,1表示向右,0表示两车相遇)。

样例 1

输入:5 2

   4 1

   5 -1

   7 1

   2 1

   6 -1

输出:4 0

   4 0

   9 1

   3 -1

   6 1


相关知识点:模拟


题解


求解本题必须认识到以下两个关键:

  1. 对于任意碰碰车,其无论经过多少次碰撞以及多长的时间,它的相对位置始终都是固定不变的。例如,对于题目给出示的例子而言,无论其过了多长的时间,第 i i i 辆车将始终处于第 i i i 个位置。这为我们提示了一种(对碰碰车进行处理后如何还原)原始碰碰车的排列思路。

碰碰车

  1. 如果不考虑所有碰碰车之间的差异(即视所有车都一样),且假定这些车不会发生碰撞实(即,假设当前存在 n n n 条车道),则在这种情况下,所有车在经过任意时间后的位置实际上与其会发生碰撞时得到的状态是一致的。

不碰车

基于以上两点特性,我们可以总结出:若对全部碰碰车执行 “无差别无碰撞” 的行驶状态模拟,则该模拟最终得到的状态中,各碰碰车的相对位置与其绝对位置的映射关系和 “有差别有碰撞” 情况下的映射关系一致。因此,我们可以通过这一映射关系来巧妙地得到各碰碰车的最终位置。其建立过程如下:

  1. 将一开始录入的碰碰车顺序保存进一个数组中(此为绝对位置)。例如,在上图例子中,题目给出的碰碰车顺序为:
  • 第 1 条数据:橙色车
  • 第 2 条数据:绿色车
  • 第 3 条数据:黑色车
  • 第 4 条数据:蓝色车
  • 第 5 条数据:黄色车
  1. 按各车实际位置排序,并得到各自的相对位置:
  • 第 1 条数据:蓝色车(位置:2)
  • 第 2 条数据:橙色车(位置:4)
  • 第 3 条数据:绿色车(位置:5)
  • 第 4 条数据:黄色车(位置:6)
  • 第 5 条数据:黑色车(位置:7)
  1. 基于此,可建立由相对位置到绝对位置的映射关系为:
    { 1 → 4 , 2 → 1 , 3 → 2 , 4 → 5 , 5 → 3 } \{ 1 \rightarrow 4, 2 \rightarrow 1, 3 \rightarrow 2, 4 \rightarrow 5, 5 \rightarrow 3 \} { 14,21,32,45,53}

  2. 接下来不考虑各车的差异和碰撞,直接遍历 2 中的所有车,模拟各车行驶:

  • 第 1 辆车(蓝),行驶 t 秒后的位置:4
  • 第 2 辆车(橙),行驶 t 秒后的位置:6
  • 第 3 辆车(绿),行驶 t 秒后的位置:3
  • 第 4 辆车(黄),行驶 t 秒后的位置:4
  • 第 5 辆车(黑),行驶 t 秒后的位置:9
  1. 接下来再次根据各车的实际位置排序,这将产生新的相对位置(注意,尽管有的车位置一样,但排列在数组中时,其索引肯定是不一样的,但是不管索引谁大谁小,都不影响最终的输出):
  • 第 1 位: 位置 3(绿)
  • 第 2 位: 位置 4(蓝)
  • 第 3 位: 位置 4(黄)
  • 第 4 位: 位置 6(橙)
  • 第 5 位: 位置 9(黑)
  1. 然后,根据前面存储的 “相对位置到绝对位置的映射” 表可将上面得到的相对位置对转变为绝对位置:
  • 第 1 位(1→4),故作为第 4 条输出(位置:3)
  • 第 2 位(2→1),故作为第 1 条输出(位置:4)
  • 第 3 位(3→2),故作为第 2 条输出(位置:4)
  • 第 4 位(4→5),故作为第 5 条输出(位置:6)
  • 第 5 位(5→3),故作为第 3 条输出(位置:9)
  1. 最后,为了保证输出顺序与输入时的一致,我们再对所有数据按其绝对位置排列,并输出,即有:
  • 第 1 行: 4
  • 第 2 行: 4
  • 第 3 行: 9
  • 第 4 行: 3
  • 第 5 行: 6

根据这样的思路,可采取以下方法来计算碰碰车的最终位置:

  1. 设计一个结构体 Car,该结构体包含当前碰碰车在输入时的序号(index)(指示了各碰碰车的输入顺序,作为绝对位置)、位置(position)方向(direction)。接下来就能通过此结构体构建数组,用以存放所有的输入信息:即 Car cars[MAX];
  2. 从关键点 1 可知,无论碰碰车怎么移动都不会改变其相对位置,因此我们首先需要根据碰碰车的位置(position)来进行排序,这样就能将所有碰碰车的相对位置确定下来(此时相对位置通过排序后的 cars[] 数组索引指示);
  3. 接下来遍历排序过后的 cars 数组,以建立相对位置到绝对位置的映射关系。同时,还要对其中的每个碰碰车进行状态改变(忽视各车之间的碰撞和差异)。这里我们利用了关键点 2 的信息:“若忽视各车之间的碰撞,让他们保持行驶,则最终各车所处位置和不忽视的情况是一致的”;
  4. 然后,再次根据位置(position)属性对 cars 数组排序,这时,排序得到 cars 数组的索引,即是各车在 “忽视各车之间的碰撞” 的前提下的新相对位置。而关键点 2 告诉我们,这里得到的相对位置和真实情况下(考虑碰撞)的绝对位置之间的映射关系是不变的。这也就是说,此处可以通过前面建立的 “索引映射” 来还原各相对位置上的碰碰车的 序号(index)
  5. 最后,由于本题要求按照指定的碰碰车输入顺序予以输出(即,题目认为各车之间存在差异),因此我们可再次对 cars 数组以 序号(index) 属性进行排序。将排序后的 cars 数组按顺输出即得到了所需答案。

下面给出按以上思路写出的完整代码:

/*
	碰碰车
	 
*/


#include<bits/stdc++.h> 
using namespace std;

const int MAX= 1005;
struct Car{
    
    
	int position, direction, index;
};
Car cars[MAX];
int indexArray[MAX];

bool cmp1(Car c1, Car c2){
    
    
	return c1.position<c2.position;
}

bool cmp2(Car c1, Car c2){
    
    
	return c1.index<c2.index;
}

int main( )
{
    
    
	// 获取输入 
	int n, t;cin>>n>>t;
	for(int i=0;i<n;i++){
    
    
		cin>>cars[i].position>>cars[i].direction;
		cars[i].index = i;
	}
	
	// 按位置排序(以获取各车的相对位置) 
	sort(cars, cars+n, cmp1); 
	
	// 模拟碰碰车的行驶过程(忽视碰撞)并记录相对位置 
	for(int i=0;i<n;i++){
    
    
		
		// 记录各车相对位置到绝对位置的映射 
		// 数组 indexArray 的索引是某个车的相对位置;存储值为该相对位置对应的绝对位置
		indexArray[i] = cars[i].index;
		
		// 开车开车
		cars[i].position += cars[i].direction*t;
	} 
	
	// 再次按位置排序,得到各车行驶 t 秒后的相对位置(此值被 cars 数组的索引指示) 
	sort(cars, cars+n, cmp1);
	
	// 根据相对位置到绝对位置的映射记录来还原各车的绝对位置
	for(int i=0;i<n;i++){
    
    
		
		// 还原各车的绝对位置
		cars[i].index = indexArray[i]; 
		
		// 注意将那些“相撞”的车的方向置为 0
		if(i>0 && cars[i].position==cars[i-1].position ||
		 i<n-1 && cars[i].position==cars[i+1].position) 
		 	cars[i].direction = 0;
	} 
	
	// 按绝对位置排序
	sort(cars, cars+n, cmp2); 
	
	// 输出
	for(int i=0;i<n;i++) cout<<cars[i].position<<" "<<cars[i].direction<<endl; 
	 
    return 0;
}


MT2036 移水造海

难度:钻石  时间限制:1秒  占用内存:128M
题目描述

今天,无聊的小码哥打开了他的 tr,他突发奇想,“我要把一个新世界的一块大陆用水桶运水填成海”,尽管这个点子无聊透顶,但他真的去做了。

由于小码哥使用了修改器,他拥有了无限的水桶。一个水桶的水能够刚好填上一块格子。但如果倒入水后水位高于两边中一边土地,那么水会溢出,这桶水相当于没倒过。
另外,世界左右两侧是虚空,水碰到虚空会消失。

现在给了你这个世界的地形图,告诉你这个世界的宽度 n 以及每一列的土地的高度 h,问你至少要多少水桶的水才能填满这个世界(填满即无论在哪里倒一桶水都会溢出)。

格式

输入格式:第一行一个正整形 n ( n ≤ 10000 ) n(n≤10000) n(n10000) ,表示世界的宽度(这是个世界);

     第二行输入 n n n 个非负整数 h i h_i hi,表示第 i i i 列土地的高度

输出格式:一个整数,表示至少需要几桶水。

样例 1

输入:6

   3 0 2 0 1 0

输出:3

样例 2

输入:5

   1 0 0 0 1

输出:3

样例 3

输入:9

   4 1 2 3 6 3 2 1 3

输出:9

备注

其中: 3 ≤ n ≤ 10000 , 0 ≤ h ≤ 10000 3≤n≤10000,0≤h≤10000 3n10000,0h10000


相关知识点:模拟


题解


对题目解读后可将其描述为:对二维世界中的“山”进行填补。例如,下图展示了题目给出的第3个例子:

在这里插入图片描述

其可以填水的格子如下(共9个):

在这里插入图片描述

我们的任务就是对题目给出的任意形状的“山”,输出其对应的可填水的格子数。注意到一件事,可以被用于填水的格子应该满足:该格子所处竖直方向上的山峰高度要低于其两侧的山峰高度(这将构成一个间隙)。所以,我们需要设计算法来检测和统计指定形状的“山”中的间隙个数。

一种直观的办法是,按自下向上,自左向右的方式进行扫描。既然要按指定方向扫描,那就必须确定扫描指针的上下限。在从下往上扫描时,其目的是获取当前竖直方向上的峰高,从而便于对比。显然,峰高的最大取值即为输入数据中所有峰高的最大值,最小值为0;在从左往右扫描时,其范围正是输入的“土地高度”序列范围。除此之外,我们还必须注意一个点:“世界左右两侧是虚空,水碰到虚空会消失”。这就是说,对于“山”里某列的左右两侧而言,也必须构成间隙(两侧高中间低)。所以我们在自左向右扫描时,需要想办法来检测“间隙”的出现。

注意到“间隙”的特点:当前位置的山峰高度低于其左右两侧。因此我们可以设置一个记录比“当前层高度 mtHeight ”更高的“上一次较高峰位置指针 lastHeighter ”,用以暂存间隙的左侧山峰位置。接下来横向遍历输入的每个山峰高度,依次判断其是否大于lastHeighter?如果是,就代表当前找到一个(或若干个)“间隙”。基于这样的思路,可将此算法流程描述如下:

  1. (纵向)遍历山的高度:0~maxHeight(山中的最高峰),每次循环开始时,都设置 lastHeighter=-1,表示初始情况下默认不存在间隙条件(描述“水碰到虚空会消失”这一情况)。
  2. (横向)遍历所有山峰:如果当前山峰高度 height[j] 大于当前层高度 mtHeight,则说明找到了间隙的右侧山峰,也就是说这里出现了“间隙”。但别急!还需要判断上一次较高峰位置指针 lastHeighter 是否存在,如果存在(即其取值不为-1),才表示真的出现了“间隙”;否则就没有。当存在“间隙”时,这之间可能有多个,其数量 = “间隙右侧指针” - “间隙左侧指针” - 1。即:

“间隙” 数量 = j – lastHeighter - 1

最后,还需要将这里新出现的“较高峰位置”更新至 lastHeighter 中。

基于这样的思路,可得到求解本题的完整代码:

/*
	移水造海 
	 
*/


#include<bits/stdc++.h> 
using namespace std;

const int MAX= 10005;
int ary[MAX];

// 获取数组中的最大值 
int getArrayMax(int ary[], int n){
    
    
	int max = ary[0];
	for(int i=1; i<n; i++)
		if(ary[i] > max)
			max = ary[i];
	return max;
}

// 计算需要的水桶数量
int getBuckets(int ary[], int n){
    
    
	int bukets = 0, maxHeight = getArrayMax(ary,n), last;
	
	// 从最底层开始往上逐层扫描 
	for(int i=1; i<=maxHeight; i++){
    
    
		// 每次初始化标记上一次的层高
		last = -1;
		
		// 接下来从左往右扫描
		for(int j=0; j<n; j++)
			if(ary[j] >= i){
    
    
				if(last != -1) bukets += (j-last-1);
				
				// 更新上一次的层高 
				last = j;
			} 
	} 
	
	return bukets;
} 

int main( )
{
    
    
	// 获取输入 
	int n; cin>>n;
	for(int i=0;i<n;i++) cin>>ary[i];
	
	// 输出操作次数
	cout<<getBuckets(ary,n);
	 
    return 0;
}

END


猜你喜欢

转载自blog.csdn.net/the_ZED/article/details/130352853