5.2.1排序与检索
例题5-1:大理石在哪儿?(where is Marble?,Uva 10474)
思路:先排序,后查找。使用algorithm
头文件中的sort()
和lower_bound()
实现。
sort(first_pointer,first_pointer+n,cmp)
:第一个参数是数组的首地址,一般写上数组名就可以,因为数组名是一个指针常量,第二个参数是首地址加上数组的长度n(代表尾地址的下一地址),第三个参数可以不填,默认升序。lower_bound( begin,end,num
):在从小到大的排序数组中,从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到则返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 10000;
int main()
{
int n,q,x,a[maxn],cnt=0; //n大理石数,q问题数,x问题的数的值,a[i]用于排序,cnt用来记用例的数
while(scanf("%d%d",&n,&q) ==2&& n) //成功读入两个整数n和q,且n不为0时
{
printf("CASE# %d:\n",++cnt);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
sort(a,a+n); //排序
while(q--)
{
scanf("%d",&x);
int p = lower_bound(a,a+n,x)-a; //在已排序的数组中寻找x
if(a[p]!=x) printf("%d not found\n",x);
else printf("%d found at %d\n",x,p+1);
}
}
return 0;
}
5.2.2 不定长数组:vector
思路:本质是数组的序列插入和删除问题。在模拟的时候如果直接按照题目给的指令写函数容易代码重复比较冗长,此时应该找到指令之间的共同点来编写函数以优化代码。
- vector就是一个不定长数组。可以用
a.size()
读取它的大小,a.resize()
改变大小,a.push_back()
向尾部添加元素,a.pop_back()
删除最后一个元素。 - vector是一个模板类,需要用
vector<int> a
这样来声明,vector可以直接赋值,还可以作为函数的参数或者返回值。
例题5-2:木块问题(The Blocks Problems,Uva 101)
代码:
/*紫书第五章 例题5-2 木块问题(The Blocks Problem,Uva 101)*/
#include<bits/stdc++.h>
using namespace std;
const int maxn = 30;
int n;
vector<int> pile[maxn]; //二维数组,每个pile[i]是一个vector
//找到木块a所在的堆pile和高度hight,以引用的方式返回调用者。
void find_block(int a,int &p,int &h){
for(p = 0;p<n;p++)
for(h=0;h<pile[p].size();h++)
if(pile[p][h] == a)
return;
}
//把第p堆高度为h的木块上方的所有木块移回原位
void clear_above(int p,int h){
for(int i = h+1;i<pile[p].size();i++){
int b = pile[p][i];
pile[b].push_back(b); //把木块b放回原位
}
pile[p].resize(h+1); //pile只应该保留下标为0~h的元素
}
//把第p堆高度为h及其上方的木块移动到p2堆的顶部
void pile_onto(int p,int h,int p2){
for(int i = h;i<pile[p].size();i++) pile[p2].push_back(pile[p][i]);
pile[p].resize(h);
}
//打印结果
void print(){
for(int i=0;i<n;i++){
printf("%d:",i);
for(int j=0;j<pile[i].size();j++) printf(" %d",pile[i][j]);
printf("\n");
}
}
int main(){
int a,b;
cin>>n ;
string s1,s2;
for(int i=0;i<n;i++) pile[i].push_back(i);
while(cin>>s1>>a>>s2>>b){
int pa,pb,ha,hb;
find_block(a,pa,ha);
find_block(b,pb,hb);
if(pa==pb) continue; //非法指令
if(s2=="onto") clear_above(pb,hb);
if(s1=="move") clear_above(pa,ha);
pile_onto(pa,ha,pb);
}
print();
return 0;
}
5.2.3集合:set
集合与映射也是两个常见的容器。set就是数学上的集合,每个元素最多只出现一次。和sort一样自定义类型也可以构造set,但必须定义“小于”运算符。
例题5-3:安迪的第一个字典(Andy‘s First Dictionary,Uva 10815)
思路:用getline()
函数读取整行,读到大写字母就转化成小写的,读到小写就存入字符串s,读到非字母就代表这个单词结束了, 将串压入set集合,清空串,循环进行。值得注意的一点是:串的长度为0,也是串,叫做“空串”。也会被压入set。所以压入前要判断一下长度是否为0。
auto
关键字可以在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型。getline(cin, inputLine):
cin 是正在读取的输入流,而 inputLine 是接收输入字符串的 string
变量的名称。stringstream类
:需要在程序中使用字符串和数字数据互相转换的时候使用,需要在源程序文件中包含头文件include<sstream>
。stringstream对象的使用方法与cout对象的使用方法基本相同。需要将程序中的数据保存在一个string中的时候,可以先创建一个stringstream对象,并通过运算符<<
将数据传递给 stringstream 对象。(这与通过<<
使用cout对象的方法相同。)接着通过调用stringstream 类的函数str()
将对象所包含的内容赋给一个string对象。
代码:
/*紫书第五章 例题5-3 安迪的第一个字典(Andy‘s First Dictionary,Uva 10815)*/
#include<bits/stdc++.h>
using namespace std;
string text, s;
set<string> dict; // 词典
int main() {
while (getline(cin, s)) { //一次取一行
for (auto& ch : s) { //依次取出s中的元素,ch是s中每个字符的引用,对ch进行修改也就可以修改s本身了
if (isalpha(ch)) ch = tolower(ch); // 判断是否是字母,遇到大写字母就替换为小写
else ch = ' '; // 把所有非字母的字符替换为空格
}
text += " "+s; // 拼接文本,注意空格,避免两行尾首直接相连
}
stringstream input(text); // 按空格分割
while (input >>s) dict.insert(s); // 插入去重
for (auto p : dict) cout <<p <<endl;
return 0;
}
5.2.4 映射:map
map是key到value的映射,map內部的实现自建一颗红黑树,这颗树具有对数据自动排序的功能。因为重载了[ ]
运算符,更像是高级的数组。例如可以用一个map<string,intd>month_name
来表示“月份名字到月份编号”的映射,然后用month_name[“july”]=7
的方式来赋值。
例题:5-4 反片语(Ananagrams,Uva 156)
思路:定义set<string> ans;
存储输出结果,自动按字典序排列
遍历每个标准化单词,若其出现次数为1,插入ans集合中,最后输出即可。
代码:
#include<bits/stdc++.h>
using namespace std;
map<string, int> dict; // 标准化单词->出现次数
map<string, string> trans; // 单词字母出现次数->原单词
set<string> ans; // 存储输出结果,按字典序排列
string s, st;
int main() {
while (cin >>s && s!= "#") {
st = s;
for (auto& ch : st)
if (ch >= 'A' && ch <= 'Z') ch = tolower(ch); // 转为小写
sort(st.begin(), st.end()); // 升序排列,顺序无关
dict[st]++; // 统计次数
trans[st] = s; // 记录原单词
}
for (auto p : dict) if (p.second == 1) ans.insert(trans[p.first]); // 当value为1表示只出现一次是非重排单词,就key放到set里
for (auto& p : ans) cout <<p <<endl; // 直接输出结果集中的的单词
return 0;
}
5.2.5栈、队列与优先队列
例题:5-5 集合栈计算机(The Set Stack Comper,Uva 12096)
map和vector实现双射关系+集合的交并运算的STL
至于集合的交并操作可用algorithm
中的set_union,set_intersection
实现,注意其第五个参数是构造一个存储插入的迭代器,inserter
是特殊的插入迭代器,父类为iterator
,t和t.begin()
分别表示存储结果的容器和开始位置。
思路:对于集合的集合,我们很难直接表示,既然集合的集合难以表示,我们就只需要给每种集合一个唯一的ID就可以了,这样,集合中的元素就可以通过ID来表示。一个集合就可以表示为一个set
在这里,我们使用STL中的set进行表示,就会容易很多,加入栈中的元素也就可以是int类型了。
在进行操作时,我们可以用map将每种集合与对应的ID关联起来,这样做既可以完成查找ID的任务,还可以同时判定是否出现了新的集合。
我们可以用vector作为存储每种集合的cache,这样,每当map中没有相应的ID时,我们就向vector中加入一个set元素,并将下标作为ID进行唯一的标识。
使用vector将set存储起来的好处是,反过来我们也可以用ID查询到对应的set,这样,通过map和vector,我们实现了set 到ID 的双射。
最后,输出栈顶集合的size属性,即可。
代码:
//UVA12096 集合栈计算机
#include<bits/stdc++.h>
using namespace std;
#define ALL(x) x.begin(),x.end() //“所有的内容”
#define INS(x) inserter(x,x.begin()) //“插入迭代器”注意宏的括号和inserter
typedef set<int> Set; // 对于任意集合s,IDCache[s]是它的ID,setcache[IDcache[s]]是它本身
map<Set,int> IDCache; // 集合和对应id的映射:集合->id
vector<Set> setCache; // 哈希表:集合id->集合
int t,n; //t测试用例的数目,n操作指令的数目
char op[10];
int getID(Set s){ //查找给定集合s的ID,若果找不到就分配一个新ID
if(IDCache.count(s))return IDCache[s];
setCache.push_back(s); //将新集合加入Setcache
return IDCache[s]=setCache.size()-1;//将ID加入map ,同时返回新分配的ID值
}
int main(){
scanf("%d",&t);
while(t--){
scanf("%d",&n);
// setCache.clear();
stack<int> s;
while(n--){
scanf(" %s",&op);
if(op[0]=='P')s.push(getID(Set()));
else if(op[0]=='D')s.push(s.top());
else{
Set s1=setCache[s.top()];s.pop();
Set s2=setCache[s.top()];s.pop();
Set x;
if(op[0]=='U')set_union(ALL(s1),ALL(s2),INS(x)); //取集合并集
if(op[0]=='I')set_intersection(ALL(s1),ALL(s2),INS(x)); //取集合交集
if(op[0]=='A'){ x=s2; x.insert(getID(s1)); }
s.push(getID(x));
}
printf("%d\n",setCache[s.top()].size());
}
puts("***");
}
return 0;
}
例题 5-6:团体队列(Team Queue,Uva 540)
思路:本题有两个队列,每个团队有一个队列,团队整体又形成一个队列。
代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1000 + 10;
int main(){
int t,kase = 0;
while(scanf("%d",&t)==1&&t){
printf("Scenario #%d\n",++kase);
//记录所有人的团队编号,team[x]表示编号为x的人所在的团队编号
map<int,int> team; //个人编号->团队编号
for(int i=0;i<t;i++){
int n,x; //n团队数目,x个人
scanf("%d",&n);
while(n--){
scanf("%d",&x);
team[x] = i;
}
}
queue<int> q,q2[maxn]; //q是团队的队列,q2[i]是团队i成员的队列
for(;;){
int x;
char op[10];
scanf("%s",op);
if(op[0]=='S') break;
else if(op[0]=='D'){ //长队队首出队
int t = q.front();
printf("%d\n",q2[t].front());
q2[t].pop();
if(q2[t].empty())
q.pop(); //团体t全体出队列
}
else if(op[0]=='E'){
scanf("%d",&x);
int t = team[x];
if(q2[t].empty())
q.push(t); //团队t全体成员入队
q2[t].push(x);
}
}
printf("\n");
}
return 0;
}
例题 5-7:丑数(Ugly Numbers,Uva 136)
思路:暴力求解会超时,因此可采用优先队列构造法。从1开始,对于x,2x,3x,5x均为丑数。因此,初始将1推入优先队列,每次从优先队列取出最小值,然后2x,3x,5x推入优先队列,有些数可能会重复构造,因此用set去重。
代码:
//Uva 136
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int base[3] = {2,3,5};
int main(){ //每次取出最小的丑数,并生成三个新的丑数
priority_queue<LL,vector<LL>,greater<LL> > pq; //优先取最小的优先队列
set<LL> s; //判重
pq.push(1);
s.insert(1);
for(int i=1;;i++){
LL x = pq.top();
pq.pop();
if(i==1500){
cout<<"The 1500'th ugly number is "<<x<<".\n";
break;
}
for(int j=0;j<3;j++){
LL x2 = x*base[j];
if(!s.count(x2)){ //如果该丑数之前没有生成过就加入集合,并入队
s.insert(x2);
pq.push(x2);
}
}
}
return 0;
}