第五章 C++与STL入门(例题篇:STL部分)
例题5-1:大理石在哪儿(UVa10474) P108
排序与检索
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 10000;
int main()
{
/*ios::sync_with_stdio(false);*/
freopen("input.txt", "r", stdin);
int n, q, x, a[maxn], kase = 0;
while (scanf("%d%d", &n, &q) == 2 && n)
{
printf("CASE# %d:\n", ++kase);
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;
if (a[p] == x)
{
printf("%d found at %d\n", x, p + 1);
}
else
{
printf("%d not found\n", x);
}
}
}
return 0;
}
代码中使用了algorithm头文件中的sort和lower_bound;loer_bound的作用是查找“大于或者等于x的第一个位置”。
例题5-2:木块问题(The Blocks Problem,UVa101) P110
不定长数组:vector
#include<iostream>
#include<vector>
#include<string>
using namespace std;
const int maxn = 30;
int n;
vector<int> pile[maxn];
//找到木块a所在的pile和height
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);
}
//把第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;
}
上述代码一个值得学习的技巧:输入一共有4种指令,但如果完全独立地处理各指令,代码就会变得冗长而且易错。更好的方法是提取出指令之间的共同点,编写函数以减少重复代码。
vector头文件中的vector是一个不定长数组,可以用clear()清空,resize()改变大小,用push_back()和pop_back()在尾部添加和删除元素,用empty()测试是否为空。
例题5-3:安迪的第一个字典(Andy’s First Dictionary,UVa10815) P112
集合:set
输入案例:
Adventures in Disneyland
Two blondes were going to Disneyland when they came to a fork in the
road.The sign read:“Disneyland Left.”
So they went home.
#include<iostream>
#include<set>
#include<sstream>
using namespace std;
set<string> dict;
int main()
{
/*ios::sync_with_stdio(false);*/
freopen("input.txt", "r", stdin);
string s, buf;
while (cin >> s)
{
for (int i = 0;i < s.length();i++)
{
if (isalpha(s[i]))
s[i] = tolower(s[i]);
else
s[i] = ' ';
}
stringstream ss(s);
while (ss >> buf)
dict.insert(buf);
}
for (set<string>::iterator it = dict.begin();it != dict.end();it++)
{
cout << *it << "\n";
}
return 0;
}
set就是数学上的集合——每个元素最多只出现一次。和sort一样,自定义类型也可以构造set,但同样必须定义“小于”运算符。
set::iterator,iterator的意思是迭代器,是STL中的重要概念,类似于指针。
例题5-4:反片语(Ananagrams,UVa156) P113
映射:map
#include<iostream>
#include<map>
#include<vector>
#include<algorithm>
#include<string>
using namespace std;
map<string, int> cnt;
vector<string> words;
//将单词s进行“标准化”
string repr(const string&s)
{
string ans = s;
for (int i = 0;i < ans.length();i++)
{
ans[i] = tolower(ans[i]);
}
sort(ans.begin(), ans.end());
return ans;
}
int main()
{
freopen("input.txt", "r", stdin);
int n = 0;
string s;
while (cin >> s)
{
if (s[0] == '#')
break;
words.push_back(s);
string r = repr(s);
if (!cnt.count(r)) cnt[r] = 0;
cnt[r]++;//要有cnt[r]存在才会加
}
vector<string> ans;
for (int i = 0;i < words.size();i++)
{
if (cnt[repr(words[i])] == 1)
ans.push_back(words[i]);
}
sort(ans.begin(), ans.end());
for (int i = 0;i < ans.size();i++)
{
cout << ans[i] << "\n";
}
return 0;
}
此例说明,如果没有良好的代码设计,是无法发挥STL的威力的。如果没有想到“标准化”这个思路,就很难用map简化代码。
例题5-5:集合栈计算机(The SetStack Computer,UVa12096) P115
栈:stack
输入案例:
1
5
PUSH
DUP
UNION
PUSH
ADD
#include<iostream>
#include<set>
#include<map>
#include<vector>
#include<stack>
#include<string>
#include<algorithm>
#include <iterator> //inserter的头文件
using namespace std;
typedef set<int> Set;
map<Set, int> IDcache;//把集合映射成ID
vector<Set> Setcache;//根据ID取集合
//查找给定集合x的ID。如果找不到,分配一个新ID
int ID(Set x)
{
if (IDcache.count(x))
return IDcache[x];
Setcache.push_back(x);
return IDcache[x] = Setcache.size() - 1;
}
#define ALL(x) x.begin(),x.end()
#define INS(x) inserter(x,x.begin())
int main()
{
/*ios::sync_with_stdio(false);*/
freopen("input.txt", "r", stdin);
int T;
cin >> T;
while (T--)
{
stack<int> s;
int n;
cin >> n;
for (int i = 0;i < n;i++)
{
string op;
cin >> op;
if (op[0] == 'P')
s.push(ID(Set()));
else if (op[0] == 'D')
s.push(s.top());
else
{
Set x1 = Setcache[s.top()];
s.pop();
Set x2 = Setcache[s.top()];
s.pop();
Set x;
if (op[0] == 'U')
set_union(ALL(x1), ALL(x2), INS(x));
if (op[0] == 'I')
set_intersection(ALL(x1), ALL(x2), INS(x));
if (op[0] == 'A')
{
x = x2;
x.insert(ID(x1));
}
s.push(ID(x));
}
cout << Setcache[s.top()].size() << endl;
}
cout << "***" << endl;
}
return 0;
}
例题5-6:团队队列(Team Queue,UVa540) P117
队列:queue
STL队列定义在头文件中,用“queues”方式定义,用push()和pop()进行元素的入队和出队操作,front()取队首但不删除。
#include<cstdio>
#include<map>
#include<queue>
using namespace std;
const int maxt = 1000 + 10;
int main()
{
freopen("input.txt", "r", stdin);
int t, kase = 0;
while (scanf("%d", &t) == 1 && t)
{
printf("Scenario #%d\n", ++kase);
//记录所有人的团队编号
map<int, int> team;
for (int i = 0;i < t;i++)
{
int n, x;
scanf("%d", &n);
while (n--)
{
scanf("%d", &x);
team[x] = i;
}
}
queue<int> q, q2[maxt];
for (;;)
{
int x;
char cmd[10];
scanf("%s", cmd);
if (cmd[0] == 'S')
break;
else if (cmd[0] == 'D')
{
int t = q.front();
printf("%d\n", q2[t].front());
q2[t].pop();
if (q2[t].empty())
q.pop();
}
else if (cmd[0] == 'E')
{
scanf("%d", &x);
int t = team[x];
if (q2[t].empty())
{
q.push(t);
}
q2[t].push(x);
}
}
printf("\n");
}
return 0;
}
例题5-7:丑数(Ugly Numbers,UVa136) P120
优先队列:priority_queue
STL的queue头文件提供了优先队列,用“priority_queue s”方式定义,用push()和pop()进行元素的入队和出队操作,top()取队首元素(但不删除,对应queue是front())。
“priority_queue pq”,pq是一个“越小的整数优先级越低的优先队列”。自定义类型也可以组成优先队列,但必须为每个元素定义一个优先级。这个优先级并不需要一个确定的数字,只需要能比较大小即可。只要元素定义了“小于”运算符,就可以使用优先队列。在一些特殊的情况下,需要使用自定义方式比较优先级,例如,要实现一个“个位数大的整数优先级反而小”的优先队列,可以定义一个结构体cmp,重载“()”运算符,使其“看上去”像一个函数,然后用“priority_queue<int,vector,cmp>pq”的方式定义。下面是这个cmp的定义:
struct cmp {
bool operator() (const int a, const int b)const
{//a的优先级比b小时返回true
return a % 10 > b % 10;
}
};
对于一些常见的优先队列,STL提供了更为简单的定义方法,例如,“越小的整数优先级越大的优先队列”可以写成“priority_queue<int,vector,greater >pq”。注意,最后两个“>”符号不要写在一起,否则会被很多(但不是所有)编译器误认为是“>>”运算符。
题意:
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。
丑数是指不能被2,3,5以外的其他素数整数的数。
#include<iostream>
#include<queue>
#include<vector>
#include<set>
#include<functional>//greater的头文件
using namespace std;
typedef long long LL;
const int coeff[3] = { 2,3,5 };
int main()
{
/*ios::sync_with_stdio(false);*/
//freopen("input.txt", "r", stdin);
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*coeff[j];
if (!s.count(x2))
{
s.insert(x2);
pq.push(x2);
}
}
}
return 0;
}
不用greater定义优先级
#include<iostream>
#include<queue>
#include<vector>
#include<set>
using namespace std;
typedef long long LL;
const int coeff[3] = { 2,3,5 };
struct cmp {
bool operator() (const LL a, const LL b) const
{//a比b优先级小时返回true
return a > b;
}
};
int main()
{
priority_queue<LL, vector<LL>, cmp> 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*coeff[j];
if (!s.count(x2))
{
s.insert(x2);
pq.push(x2);
}
}
}
return 0;
}
知识点一:测试STL P121
用随机序列测试sort:
为了随机生成整数,先来看看随机数发生器。核心函数是cstdlib中的rand(),它生成一个闭区间[0,RAND_MAX]内的均匀随机整数,其中RAND_MAX至少为32767(2^15-1),在不同环境下的值可能不同。严格地说,这里的随机数是“伪随机数”,因为它也是由数学公式计算出来的。
如何产生[0,n]之间的整数呢?很多人喜欢用rand()%n产生区间[0,n-1]内的一个随机整数,姑且不论这样产生的整数是否仍然分布均匀,只要n大于RAND_MAX,此法就不能得到期望的结果。另一个方法是执行rand()之后先除以RAND_MAX,得到[0,1]之间的随机实数,扩大n倍后四舍五入,得到[0,n]之间的均匀整数。这样,在n很大时“精度”不好(好比把小图放大后会看到“锯齿”),但对于普通的应用,这样做已经可以满足要求了。
srand(time(NULL))可以初始化“随机数种子”。简单地说,种子是伪随机计算的依据。种子相同,计算出来的“随机数”序列总是相同。如果不调用srand而直接使用rand(),相当于调用过一次srand(1),因此程序每次执行时,将得到同一套随机数。
想要每次得到的随机数不同,一个简单的方法是使用当前时间time(NULL)(在ctime头文件中)作为参数调用srand。time函数返回的是自UTC时间1970年1月1日0点以来经过的“秒数”,因此每秒才变化一次。如果你的程序是由操作系统自动批量执行的,可能因为每次运行的间隔时间过短,导致在相邻若干次执行时time的返回值全部相同。一个解决办法是在测试程序的主函数中设置一个循环,做足够多次测试后再退出。
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstdlib>
#include<cassert>
using namespace std;
void fill_random_int(vector<int> &v, int cnt)
{
v.clear();
for (int i = 0;i < cnt;i++)
{
v.push_back(rand());
}
}
void test_sort(vector<int>&v)
{
sort(v.begin(), v.end());
for (int i = 0;i < v.size() - 1;i++)
{
assert(v[i] <= v[i + 1]);
}
}
int main()
{
vector<int> v;
fill_random_int(v, 1000000);
test_sort(v);
return 0;
}
测试时往往使用assert。其用法是“assert(表达式)”,当表达式为假时强行终止程序,并给出错误提示。
vector、set和map都很快,其中vector的速度接近数组(但仍有差距),而set和map的速度也远远超过了“用一个vector保存所有值,然后逐个元素进行查找”时的速度。set和map每次插入、查找和删除时间和元素个数的对数呈线性关系。
注意vector并不是所有操作都快。例如vector提供了push_front操作,但由于在vector首部插入元素会引起所有元素往后移动,实际上push_front是很慢的。