【学习笔记+习题集】字符相关(输入输出流,字典树,AC自动机,后缀自动机)(4598字)(更新至2022.12.28)

目录

板块零:输入输出流

情况一:读取字符串和读取行混用的时候

情况二:关于识别空行

第一题:hdoj2072

情况三:用char数组接受getline函数的输入流

情况四:关于汉字

补充练习:

第一题:单词反转(对行,pe地狱)

情况五:关于getchar()读取到文件尾

第一题:排序 

板块一:字典树(Trie)

注意:

操作1:建树

操作2:查找

封装成结构体:

习题集:

第一题:统计Trie的结点数

板块二:AC自动机

概念和原理:


板块零:输入输出流

阴间的输入和输出。

情况一:读取字符串和读取行混用的时候

#include <bits/stdc++.h>
using namespace std;
int main() {
	string a, b;
	cin >> a;
	cin.get();//清除回车,否则下面的getline会因为cin残留的回车符直接结束,输出回车
	getline(cin, b);
	cout << a << '\n';
	cout << b << '\n';
	return 0;
}

cin会残留回车符,就像scanf一样。

情况二:关于识别空行

#include <bits/stdc++.h>
using namespace std;
int main() {
	string a;
	getline(cin, a);
	if (a == "") {
		cout << "YES\n";
	} else {
		cout << "NO\n";
	}
	return 0;
}

 getline对空行畅行无阻,并不会停下来,不会异常返回。

专治阴间输入输出流练习题:

第一题:hdoj2072

注意坑点可能有多个空格,主要这里是每行一组,以#结束。

#include <bits/stdc++.h>
using namespace std;
int main() {
	char ch;
	set<string> s;
	string a;
	int flag = 0;//如果前面有空格的话
	while ((ch = getchar()) != '#') {//如果不是#
		if (ch <= 'z' && ch >= 'a') {
			if (flag) {//如果后面有字母,而且,前面是有空格
				s.insert(a);
				flag = 0;
				a = "";
			}
			a += ch;
		} else if (ch == ' ') {//如果最开始就是空格
			if (a != "") {//而且,a不是空串,flag等于1
				flag = 1;
			}
		} else if (ch == '\n') {//如果读到回车
			if (a != "") {//而且还有一个串
				s.insert(a);
				a = "";
				flag = 0;//注意置零以防对下一组数据产生影响
			}
			cout << s.size() << '\n';
			s.clear();//清空
		}
	}
	return 0;
}

情况三:用char数组接受getline函数的输入流

getline()函数是只能接受string类型的。 不信可以试试:p

#include <iostream>
#include <string>
using namespace std;
int main() {
	string a;
	char b[1000];
	getline(cin, a);
	return 0;
}

情况四:关于汉字

汉字占两个char,而且,两个char变量的值均为负数。

汉字统计

#include <bits/stdc++.h>
using namespace std;
const int MAX = 1e6 + 5;
char s[MAX];
int main() {
	int t;
	cin >> t;
	cin.get();
	while (t--) {
		gets(s + 1);
		long long ans = 0;
		for (int i = 1; i < strlen(s + 1); i++) {
			if (s[i] >= 0 && s[i] <= 127) continue;
			ans++;
			i++;
		}
		cout << ans << '\n';
	}
	return 0;
}

补充练习:

第一题:单词反转(对行,pe地狱)

#include <bits/stdc++.h>
using namespace std;
int main() {
	int t;
	cin >> t;
	getchar();
	while (t--) {
		string s = "";
		char ch;
		while ((ch = getchar()) != '\n') {
			if (ch == ' ') {
				for (int i =s.size() - 1; i >= 0; i--) {
					if (s[i] == ' ') continue;//小心空格
					cout << s[i];
				}
				cout << ' ';
				s = "";
			}
			s += ch;
		}
		for (int i = s.size() - 1; i >= 0; i--) {
			if (s[i] == ' ') continue;//小心空格
					cout << s[i];
		}
	    cout << '\n';
	}
	return 0;
} 

情况五:关于getchar()读取到文件尾

第一题:排序 

注意while ((ch = getchar()) != EOF) ch和getchar之间需要括号,否则优先级会出问题。

#include <bits/stdc++.h>
using namespace std;
int main() {
//	ios::sync_with_stdio(false);
//	cin.tie(0);
	priority_queue<int, vector<int>, greater<int> > q;
	char ch;
	int flag = 0, sum = 0; 
	while ((ch = getchar()) != EOF) {
		if (ch != '5' && ch != '\n') {
			flag = 1;
		}
		if (flag && ch != '5' && ch != '\n') {
			sum = 10 * sum + ch - '0';
		}
		if (flag && ch == '5') {
			flag = 0;
			q.push(sum);
			sum = 0;
		}
		if (ch == '\n') {
			if (flag) {
				q.push(sum);
				sum = 0;
				flag = 0;
			}
			int c = q.top();
			q.pop();
			cout << c;
			while (!q.empty()) {
				c = q.top();
				q.pop();
				cout << ' ' << c;
			}
			cout << '\n';
		}
	}
	return 0;
}

板块一:字典树(Trie)

字典树是一种前缀树。有邻接矩阵版本,指针链表版本和结构体版本(大概)。

注意:

关于数组开多大的问题,注意,这种方法的空间消耗很大。最大的空间不是最长子串而是所有的字母数,即单词数 * 最长单词长度,这样才稳妥不会RE。

操作1:建树

板子:

void insert(string s) {
	int p = 0;
	for (int i = 0; i < s.size(); i++) {
		int x = s[i] - 'a';
		if (trie[p][x] == 0) trie[p][x] = ++id;//如果这个点不存在则标记id结点
		p = trie[p][x];//这个类似链表中的空指针,不断移动,每次从根结点开始
	}//id是指向下一个要填的字母的位置!!!
	cnt[p]++;//标记,记录在最后一个字母的id上。id和p之间一追一赶十分精巧。
}

trie是一个二维数组,不是字符数组,有点类似邻接矩阵。其中一维是26个字母的标号,第二维是节点的编号用变量p表示,id是一个编号,用来表示单词的先后关系,用来说明此处是否有单词。cnt数组用来统计此处的单词数目。

这个id的使用是关键,当trie走到零的时候,说明这是一个之前没有走到过的一个新的节点,id++,输入第一个单词的时候,比如“abc”id是变化是1 2 3 然后我们再次输入 cba的话,id 变化是 4 5 6但是如果输入的是a b d的话,则是输入a的时候会索引到“ab”的b的p是2,然后到d的时候p到了3,此时是d然后出现了一个新的编号7

下面是p右边是x0(a)1(b)2(c)3(d)
0id = 1 14
1(p = id)2
23  7
3
45
56
6
7

三种颜色表示了三次输入,填表的过程。id是独立且唯一的,我们给前缀不同的单词进行标号,每当出现一种新的前缀就给一个新的编号。这种写法感觉空间的浪费还是有点大的。

操作2:查找

int find(string s) {
	int p = 0;
	for (int i = 0; i < s.size(); i++) {
		int x = s[i] - 'a';//注意大小写
		if (trie[p][x] == 0) return 0;
		p = trie[p][x];
	}
	return cnt[p];//标记
}

和建树基本相同。

封装成结构体:

来自oiwiki略作改动。

struct Trie {
  int nex[100000][26], cnt;
  bool book[100000];  // 该结点结尾的字符串是否存在
  void init() {//初始化 
  	memset(nex, 0, sizeof(nex));
  	memset(book, 0, sizeof(book));
  	cnt = 0;
  }
  void insert(string s) {  // 插入字符串
    int p = 0;//类似指针的一个数
    for (int i = 0; i < s.size(); i++) {
      int c = s[i] - 'a';//注意大小写,如果是单词大写的话会RE(负数越界)
      if (!nex[p][c]) nex[p][c] = ++cnt;  // 如果没有,就添加结点
      p = nex[p][c];//同上
    }
    book[p] = 1;//标记
  }
  bool find(string s) {  // 查找字符串
    int p = 0;
    for (int i = 0; i < s.size(); i++) {
      int c = s[i] - 'a';
      if (!nex[p][c]) return 0;
      p = nex[p][c];//逐个比对
    }
    return book[p];//返回
  }
} trie;

习题集:

第一题:统计Trie的结点数

裸题,注意单词的大小写!

#include <bits/stdc++.h>
using namespace std;
struct Trie {
  int nex[100000][26], cnt;
  bool book[100000];  // 该结点结尾的字符串是否存在
  void init() {//初始化 
  	memset(nex, 0, sizeof(nex));
  	memset(book, 0, sizeof(book));
  	cnt = 0;
  }
  void insert(string s) {  // 插入字符串
    int p = 0;//类似指针的一个数
    for (int i = 0; i < s.size(); i++) {
      int c = s[i] - 'A';//注意大小写,如果是单词大写的话会RE(负数越界)
      if (!nex[p][c]) nex[p][c] = ++cnt;  // 如果没有,就添加结点
      p = nex[p][c];//同上
    }
    book[p] = 1;//
  }
  bool find(string s) {  // 查找字符串
    int p = 0;
    for (int i = 0; i < s.size(); i++) {
      int c = s[i] - 'A';
      if (!nex[p][c]) return 0;
      p = nex[p][c];
    }
    return book[p];
  }
} trie;
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	string s;
	trie.init();
	while (cin >> s) {
		trie.insert(s);
	}
	cout << trie.cnt + 1 << '\n';//有一个root 
	return 0;
}

板块二:AC自动机

概念和原理:

AC自动机:Aho-Corasick automaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。(并不能自动AC: P

批注1:AC自动机是基于Trie树的数据结构结合KMP算法的算法。

应用:一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值