[关闭]
@fcxxzux 2017-02-16T23:20:47.000000Z 字数 6177 阅读 865

Learning a Part of C++(for ACM/ICPC) (7) STL中的string和bitset

然后我们来讲解一下STL中的2个,呃,存特定类型数据的类。

1、字符串类 string

在前一讲,我们提到,如果我们要基于内容进行比较、排序,那么char[][]存储的,不一定那么方便了。
比如

  1. char name[200][50];

我要把这些name按字典序排……holy *……

这时候,除了自己写一个结构体+定义小于号以外,你还有一个选择——string类。

我们将用例题,来展示string的使用方法。

例题1:

给你一个长度为n的名单,名单上的人需要去拜访,之后有一个长度为m的清单,表示拜访过的人。
注意:1、这个名单上的名字,大小写不敏感
2、这个拜访过的人的清单中,可能会有n人名单以外的人
请输出还有几个人没有拜访过。
每组数据,第一行2个整数n m,含义如上,之后n行,是需要拜访的名单,再之后m行,是拜访过的人。

样例数据:

5 3
Inkfish
Henry
Carp
Max
Jericho
Carp
Max
Carp

输出:

3

大体思路:把名单排序,之后用二分查找,把相应姓名标记为1,最后统计有多少个没有标为1就行了。

  1. #include <stdio.h>
  2. #include <ctype.h>
  3. #include <string.h>
  4. #include <algorithm>
  5. #include <string>
  6. #include <iostream>
  7. using namespace std;
  8. typedef long long ll;
  9. char tmp[15];
  10. string s[20005];
  11. bool vis[20005];
  12. int main(){
  13. int n,m;
  14. while(~scanf("%d%d",&n,&m)){
  15. for(int i=0;i<n;i++){
  16. scanf("%s",tmp);
  17. for(int j=0;tmp[j];j++)tmp[j]=toupper(tmp[j]);
  18. // !!! 注意点 1 !!!
  19. s[i]=tmp;
  20. }
  21. s[n]="";
  22. fill(vis,vis+n,0);
  23. // !!! 注意点 2 !!!
  24. sort(s,s+n);
  25. int cnt=0;
  26. for(;m--;){
  27. scanf("%s",tmp);
  28. for(int j=0;tmp[j];j++)tmp[j]=toupper(tmp[j]);
  29. string x=tmp;
  30. // !!! 注意点 2 !!!
  31. int it=lower_bound(s,s+n,x)-s;
  32. if(it<n&&!vis[it]&&s[it]==x){
  33. vis[it]=1;
  34. cnt++;
  35. }
  36. }
  37. printf("%d\n",n-cnt);
  38. }
  39. return 0;
  40. }

可以看到,上面的代码,使用C风格的输入输出,也可以和string友好相处。
主要要感谢string类的一个构造函数:
string (const char* s);
通过这个构造函数,我们可以很方便的,把char[]/char *转化成string类的对象了。
转化方法就如注意点1的写法:

  1. char tmp[15];
  2. string x;
  3. x=tmp;

而且,由于C++的隐式类型转换(还记得前面几何模板中,为什么不鼓励把参数设置默认值吗?),导致,下面的写法也是可行的:

  1. char* filename="a.jpg";
  2. string command = string("wget.exe -O ")+ filename + " " + "http://acm.hdu.edu.cn";

转化成string以后,我们就可以自如地按字典序排序、二分查找了(注意点2):

  1. string s[20000];
  2. sort(s,s+n);
  3. int idx=lower_bound(s,s+n,x)-s;

例题2:The Smallest String Concatenation
题意:给n(n<=50000)个字符串,请把这n个字符串重新排列并拼接起来,使得最后得到的字符串的字典序最小。

题目的做法不是重点,简单介绍一下:
自定义排序规则,对a和b 2个字符串,a+b<b+a的时候,让a排在b的前面。
详细证明,请参见:http://codeforces.com/blog/entry/43538?locale=en

然后我们直接看代码:

  1. #include <algorithm>
  2. #include <iostream>
  3. #include <string>
  4. using namespace std;
  5. typedef long long ll;
  6. string s[50005];
  7. int main(){
  8. ios::sync_with_stdio(false);
  9. int T,n;
  10. cin>>n;
  11. for(int i=0;i<n;i++)cin>>s[i];
  12. sort(s,s+n,[](const string& a,const string& b){
  13. return a+b<b+a;
  14. });
  15. for(int i=0;i<n;i++)cout<<s[i];
  16. cout<<"\n";
  17. return 0;
  18. }

这里,我直接用cin/cout来实现输入输出了。

Tips
如果要使用cin/cout,为了保证效率,请:

  • 加上ios::sync_with_stdio(false);,来不让cin/coutscanf/printf同步
    • 作为代价,请不要在这时候混用cin/coutscanf/printf,不然很容易导致Wrong Answer

排序的时候,第三个参数直接用lambda表达式了(所以提交的时候请选择带C++11/C++14/C++17字样的)。
可以看到,a+b<b+a一句,就有2个自定义运算符了:' + '和 ' < '。
+用于拼接2个字符串,各种比较运算也是应有尽有(按字典序比较的)。


篇幅有限,以及我真想不到更多我用了string的例子了,
(其他情况下,我个人会偏好使用char[]),就先到这里了。

更多string的使用方法,可以参考:
http://www.cplusplus.com/reference/string/string/

这里列举一些其他也常用的,但是这里所没有提及的,string的成员函数/运算符:

  1. string s="abc";
  2. //operator[]:你可以通过它来访问、修改一个字符。
  3. s[1]='z'; // s=="azc";
  4. //.length()/.size():可以获取string的长度。
  5. printf("%d\n",s.size()); // 3
  6. //.insert():可以在字符串中,在指定位置处,插入一个其他的字符串
  7. s.insert(1,"kkk"); //s=="akkkzc";
  8. //.erase():可以删除字符串中,从指定位置开始的,指定个数的字符
  9. s.erase(1,3); //s=="azc";
  10. //!!!注意!!!
  11. //.insert()和.erase()都是时间复杂度O(n)的操作(因为本质是操纵数组)
  12. //.c_str():返回一个c风格的等价字符串,可以让printf()输出了。
  13. printf("%s\n", s.c_str() );

2、位向量类 bitset

相信大家都知道计数排序的思想:
当可能的数值范围有限的时候,开一个数组,下标范围对应可能取值,然后某个下标对应的值,就表示这种数出现了多少份。

那么,我们现在需要知道的是,某个东西有没有出现过。而且希望你内存用得尽可能的少。
这个时候,我们自然想到了bool这种类型。
但是,很遗憾,bool,也要占用1个字节。
而1个字节,对应8个2进制位啊。
表示有还是没有,只要1个二进制位就行了,这用8个二进制位太浪费了。

那我们自己有没有办法实现啊?
有,用位运算的方法就行了。
(位运算这里不讲解了,C语言课内容,具体请自学,Matrix67之前 有个位运算讲稿的,比较合适。)

有没有什么便捷的写法呢?
STL提供了bitset类,非常适合这种情况下的需要。

bitset的使用,我们还是结合例题来讲解吧。


例题1:哈理工OJ 2029 二十世纪八十年代

大意:
我们定义,名字字符串,为一个长度不超过5的,小写字母组成的字符串。
给出长为n的名单,每一条为名字字符串。之后再给m个查询,每条查询也是一个名字字符串,要求确认待查询的字符串是否在名单中出现过。
要求:名单内的名字数量不超过200万,内存限制5MB。

我们先计算一下内存:
如果直接全部存下来:
存不下啊!

考虑换个思路:
我们把a看作1,b看作2,..., z看作26,aa看作27,一直往下。
那么,最大的zzzzz,是什么呢?

那我们开一个bool[]可以吗?
一个bool 1字节,12356630字节,这有10MB多了……
考虑bitset——一个元素1个bit(1个位),这样只占用空间:12356630/8/1024/1024=1.473MB
——非常好!5MB以内了!

然后,我们来看看下面的代码,了解bitset的使用姿势:

  1. #include <string.h>
  2. #include <iostream>
  3. #include <bitset>
  4. using namespace std;
  5. bitset<12360000> buy;
  6. char name[6];
  7. int getid(){
  8. cin>>name;
  9. int ret=0,len=strlen(name);
  10. for(int i=0;i<len;i++)
  11. ret=ret*26+(name[i]-'a'+1);
  12. return ret;
  13. }
  14. int main()
  15. {
  16. std::ios::sync_with_stdio(false);
  17. cin.tie(0);
  18. cout.tie(0);
  19. int n;
  20. for(cin>>n;n--;){
  21. buy[getid()]=1;
  22. }
  23. for(cin>>n;n--;){
  24. cout<<(buy[getid()]?"yes\n":"no\n");
  25. }
  26. return 0;
  27. }

首先注意到bitset的声明
bitset<12360000> buy;
bitset模板类需要1个参数,表示要预留多少个位置。
注意,这个参数必须是常量,而不是变量。

bitset提供了下标运算符,
你可以通过下标,然后直接赋值,
你还可以通过下标,获取某一个位置上的当前状态,是true还是false


例题2:格雷码

  Gray code is an interesting code sequence and has many applications in computer science. No matter you have known it before or not, here are some introductions about its features:
  (1) Gray code has 2n unique elements;
  (2) Each element contains n digits of 0 or 1;
  (3) Each pair of adjacent elements has exactly one different digit.
  For example, when n=2, one of the gray code sequence is: 00,01,11,10.
Now, the task is quite simple, given a positive integer n, generate the corresponding Gray code sequence.

这里说一下格雷码的生成规则:
最高位不动(gray[n-1]=a[n-1]),
其他位,是原数二进制表示的当前位异或高一位(gray[i]=a[i]^a[i+1])

然后我们要把二进制每一位都取出来……
这个时候,我们不妨看看bitset

  1. #include <stdio.h>
  2. #include <bitset>
  3. using namespace std;
  4. int main(){
  5. int n;
  6. while(scanf("%d",&n),n){
  7. for(int i=0;i<(1<<n);i++){
  8. bitset<18> s(i);
  9. for(int i=n-1;i>=0;i--){
  10. if(i==n-1)printf("%d",s[i]?1:0);
  11. else printf("%d",s[i]==s[i+1]?0:1);
  12. }
  13. puts("");
  14. }
  15. puts("");
  16. }
  17. return 0;
  18. }

bitset有一种构造函数,你可以传入一个整数,然后bitset内部将会把这个数2进制表示最低位放在[0]的位置,次低位放在[1]的位置,依次类推。
也就是说,如果传入的是5,那我们将得到:

  1. [17]=0
  2. ……
  3. [3]=0
  4. [2]=1
  5. [1]=0
  6. [0]=1

然后你就不需要和一堆位运算符一起折腾了~


上面展示的只是bitset的很小一部分,
bitset还有很多函数和重载运算符
具体请参见:http://www.cplusplus.com/reference/bitset/bitset/

下面做个简单的整理:

  1. .count(); //统计这个bitset里有多少位为1
  2. .set(); //把每一位设置为1
  3. .reset(); //把每一位设置为0
  4. .flip(); //把每一位反转
  5. .to_string(); //把bitset转换成字符串
  6. .to_ulong(); //把bitset转换成unsigned long(unsigned int)
  7. .to_ullong(); //C++11新增,把bitset转换成unsigned long long
  8. /*比如
  9. bitset<18> s(5);
  10. s.to_string(); 得"000000000000000101"
  11. s.tu_ulong(); 得 5
  12. */
  13. //注意!!!以下出现的lhs、rhs中,lhs和rhs,他们必须是同样的大小
  14. //也就是说,lhs是bitset<100>,rhs必须也是bitset<100>,其他都不行
  15. //与、或、异或
  16. bitset& operator&= (const bitset& rhs);
  17. bitset& operator|= (const bitset& rhs);
  18. bitset& operator^= (const bitset& rhs);
  19. //左移、右移
  20. bitset& operator<<= (size_t pos);
  21. bitset& operator>>= (size_t pos);
  22. bitset operator<<(size_t pos) const;
  23. bitset operator>>(size_t pos) const;
  24. //巧妙运用上面的左移、右移等位运算操作,说不好在一些类似背包的题中有奇效。
  25. //取反
  26. bitset operator~() const;
  27. //判断相等/不相等
  28. bool operator== (const bitset& rhs) const;
  29. bool operator!= (const bitset& rhs) const;
  30. template<size_t N>
  31. bitset<N> operator& (const bitset<N>& lhs, const bitset<N>& rhs);
  32. template<size_t N>
  33. bitset<N> operator| (const bitset<N>& lhs, const bitset<N>& rhs);
  34. template<size_t N>
  35. bitset<N> operator^ (const bitset<N>& lhs, const bitset<N>& rhs);
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注