赞
踩
MT2016 数据流的中位数
难度:黄金 时间限制:1秒 占用内存:128M
题目描述
对于数据流问题,小码哥需要设计一个在线系统,这个系统不断的接受一些数据,并维护这些数据的一些信息。
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
请帮小码哥设计一个支持以下两种操作的系统:
+ num 从数据流中添加一个整数 k 到系统中 ( 0 < k < 2 32 0<k<2^{32} 0<k<232 )。
? 返回目前所有元素的中位数。
格式
输入格式:第一行输入一个整形 n ( n ≤ 100000 ) n(n≤100000) n(n≤100000)。
第二行输入 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 作为关联容器都有一个问题:无法通过下标索引访问元素,必须用对应的迭代器或指针。由于 multiset 无法用索引访问元素,我们在取中位数时就必须另想办法(这里肯定不能直接迭代查询,因为迭代查询的时间复杂度为 O ( n ) O(n) O(n) ,则对 n n n 次查询的时间复杂度就达到了 O ( n 2 ) O(n^2) O(n2))。
下面给出一种 “指针固定” 的方法,来使得两个指针(左指针 l 和右指针 r)在当前容器包含的元素个数为奇数时,都指向最中间的数;当前容器包含的元素个数为偶数时,其分别指向最中间的两个数。下面的流程每次都接受一个输入数据:
获取当前容器内的元素个数 size;
容器插入当前输入数据 num;
若 size=0,将左右指针指向容器的顶部元素;
若 size 为奇数(则说明当前左指针 l 和右指针 r 指向同一个位置,且在插入 num 后 size 将变为偶数),接下来判断 num 与左指针 l (也可以选右指针 r)指向的数据的大小(以决定 num 的插入位置):
若 size 为偶数(则说明当前左指针 l 和右指针 r 指向不同位置,且在插入 num 后 size 将变为奇数),接下来需要判断 num 与左指针 l 和右指针 r 指向的数据的大小(以决定 num 的插入位置):
通过以上步骤,就能保证每次插入数据时,左指针 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} ai≤amid;同理,对任意 m i d < j mid<j mid<j 都有 a m i d ≤ a j a_{mid}≤a_j amid≤aj 。也就是说,如果将数列 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) amid≥max(A1)amid≤min(A2)
基于这一性质,我们可以维护两个长度差异不超过 1 的堆(要求堆 A 中的所有数都小于堆 B 中的数)。接下来,将堆 A 设计为大根堆,堆 B 设计为小根堆。这样,在输出中位数时只需要比较两个堆的长度:
那要如何保证前面提到的要求:堆 A 中的所有数都小于堆 B 中的数?
很简单,每次往堆里插入数据 num 时都提前判断一下 num 与当前系统中位数 Nmid 的大小关系,从而决定将其插入到哪个堆中。但有一点需要注意,我们必须维护两个堆的长度差异不超过 1!因此,具体的控制流程如下:
如此,就能始终保证 “堆 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 2≤n≤100;字符串中只包含大写字母。
相关知识点:
模拟
本题是一道非常基础的模拟题,其数据范围也很小,因此求解思路就是暴力法遍历求解(利用 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=n−1。
格式
输入格式:第一行输入正整数 n 。
输出格式:输出操作次数。
样例 1
输入:14
输出:6
备注
其中: 1 ≤ n ≤ 1 0 7 1≤n≤10^7 1≤n≤107。
相关知识点:
模拟
这道题没啥好说的,就纯模拟完事儿:
要么递推要么递推,但是鉴于数据范围,我还是觉得递推比较合适些。
/* 一秒成零 */ #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(1−100),t(0−100),分别表示:碰碰车的数量和所用时间;接下来 n n n 行,每一行都有两个数字: x , f x,f x,f 分别表示第 n n n 辆碰碰车的位置和方向 ( 0 ≤ x ≤ 1 0 5 0≤x≤10^5 0≤x≤105,-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
→
4
,
2
→
1
,
3
→
2
,
4
→
5
,
5
→
3
}
\{ 1 \rightarrow 4, 2 \rightarrow 1, 3 \rightarrow 2, 4 \rightarrow 5, 5 \rightarrow 3 \}
{1→4,2→1,3→2,4→5,5→3}
接下来不考虑各车的差异和碰撞,直接遍历 2 中的所有车,模拟各车行驶:
根据这样的思路,可采取以下方法来计算碰碰车的最终位置:
序号(index)
(指示了各碰碰车的输入顺序,作为绝对位置)、位置(position)
和方向(direction)
。接下来就能通过此结构体构建数组,用以存放所有的输入信息:即 Car cars[MAX]
;位置(position)
属性对 cars 数组排序,这时,排序得到 cars 数组的索引,即是各车在 “忽视各车之间的碰撞” 的前提下的新相对位置。而关键点 2 告诉我们,这里得到的相对位置和真实情况下(考虑碰撞)的绝对位置之间的映射关系是不变的。这也就是说,此处可以通过前面建立的 “索引映射” 来还原各相对位置上的碰碰车的 序号(index)
。序号(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(n≤10000) ,表示世界的宽度(这是个二维世界);
第二行输入 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 3≤n≤10000,0≤h≤10000。
相关知识点:
模拟
对题目解读后可将其描述为:对二维世界中的“山”进行填补。例如,下图展示了题目给出的第3个例子:
其可以填水的格子如下(共9个):
我们的任务就是对题目给出的任意形状的“山”,输出其对应的可填水的格子数。注意到一件事,可以被用于填水的格子应该满足:该格子所处竖直方向上的山峰高度要低于其两侧的山峰高度(这将构成一个间隙)。所以,我们需要设计算法来检测和统计指定形状的“山”中的间隙个数。
一种直观的办法是,按自下向上,自左向右的方式进行扫描。既然要按指定方向扫描,那就必须确定扫描指针的上下限。在从下往上扫描时,其目的是获取当前竖直方向上的峰高,从而便于对比。显然,峰高的最大取值即为输入数据中所有峰高的最大值,最小值为0;在从左往右扫描时,其范围正是输入的“土地高度”序列范围。除此之外,我们还必须注意一个点:“世界左右两侧是虚空,水碰到虚空会消失”。这就是说,对于“山”里某列的左右两侧而言,也必须构成间隙(两侧高中间低)。所以我们在自左向右扫描时,需要想办法来检测“间隙”的出现。
注意到“间隙”的特点:当前位置的山峰高度低于其左右两侧。因此我们可以设置一个记录比“当前层高度 mtHeight ”更高的“上一次较高峰位置指针 lastHeighter ”,用以暂存间隙的左侧山峰位置。接下来横向遍历输入的每个山峰高度,依次判断其是否大于lastHeighter?如果是,就代表当前找到一个(或若干个)“间隙”。基于这样的思路,可将此算法流程描述如下:
“间隙” 数量 = 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 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; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。