【马蹄集】第二十二周——进位制与字符串专题

news2024/11/26 14:39:35

进位制与字符串专题



目录

  • MT2179 01操作
  • MT2182 新十六进制
  • MT2172 萨卡兹人
  • MT2173 回文串等级
  • MT2175 五彩斑斓的串




MT2179 01操作

难度:黄金    时间限制:1秒    占用内存:128M
题目描述

刚学二进制的小码哥对加减乘除还不熟,他希望你帮他复习操作。
对于二进制数有如下几个操作:

  1. 整个二进制数加1;
  2. 整个二进制数减1;
  3. 整个二进制数乘2;
  4. 整个二进制数除2。

(本题不会导致最高位进退位,即若全是1,之后操作加1的情况不会出现)以上的操作是二进制数的对应操作,乘2或除2会导致进退位。

格式

输入格式:第一行两个正整数 n , m n,m n,m ,表示二进制数长度和操作数;
     接下来一行一个二进制数;
     第三行 m m m 个字符,+-*/,对应操作1-4。
输出格式:一行字符表示操作后的二进制数。

样例 1

输入:4 10
   1101
   */-*-*-/*/

输出:10110

备注

对于30%的数据, 1 ≤ n , m ≤ 1000 1≤n,m≤1000 1n,m1000
对于60%的数据, 1 ≤ n , m ≤ 1 0 5 1≤n,m≤10^5 1n,m105
对于100%的数据, 1 ≤ n , m ≤ 5 × 1 0 7 1≤n,m≤5×10^7 1n,m5×107


相关知识点:数学进位制


题解


对二进制数进行简单运算,最简单的办法就是将输入的二进制数字串转换为二进制数,然后再按要求进行运算并输出。但注意到一点,题目给出的二进制数的长度非常大,因此这样的求解方法不一定可行。实际上,这道题考察的是对二进制数的加减乘除操作,因此求解的关键是理解这四则运算在二进制数中的法则。

  1. 加 1 运算。二进制数的加 1 运算在末尾位为0时直接将该位置为 1,否则就需要进位(同时将该位置为 0 )。这种情况可以推广至更高位。如,对二进制数 1011 执行加 1 运算时,由于末位为 1,则加 1 会使末位发生进位,此时末位将变为 0(即得到 101’0);进位后,由于较高位的数依然为 1,则在该位也会发生这种情况,即继续进位并置当前位为 0(即得到 10’00);再次进位后,较高位为 0,则直接置该位为1即可,即得到 1100。从该过程不难看出,二进制数的加 1 运算实际上就是从低位往高位不断扫描,直到遇到一个 “0” 时,将 “0” 换为 “1” 即可。在此过程中,只要遇到 “1” 就将 “1” 换为 “0”。
  2. 减 1 运算。二进制数的减 1 运算实际上和加 1 运算是互逆的。即若二进制数的末位已经为 1,则直接将该位置为 0,否则就需要借位(同时将该位置位 1)。这种情况可以推广至更高位。如,对二进制数 1100 执行减 1 运算时,由于末位为 0,则减 1 会使末位发生借位,此时末位将变为 1(即得到 110,1);借位后,由于较高位的数依然为 0,则在该位也会发生这种情况,即继续借位并置当前位为 1(即得到 11,11);再次借位后,较高位为 1,则直接置该位为0即可,即得到 1011。从该过程不难看出,二进制数的减 1运算实际上就是从低位往高位不断扫描,直到遇到一个 “1” 时,将 “1” 换为 “0” 即可。在此过程中,只要遇到 “0” 就将 “0” 换为 “1”。
  3. 乘 2 运算。二进制数的减 2 运算只需要在二进制数的末尾添加一个 0 即可(本质上是将原二进制数向左移1位)。如二进制数 101 对应的十进制数是 5,5×2=10,而 10 对应的二进制数是 1010。
  4. 除 2 运算。二进制数的除 2 运算与乘 2 运算互逆,即只需要将原二进制数向右移 1 位。

下面给出基于上述思路得到的完整代码(已 AC):

/*
	MT2179 01操作
	用 char * 才能获得满分 
*/
#include<bits/stdc++.h> 
using namespace std;

const int MAX = 5e7+5;
char num[MAX];

int main( )
{
	// 录入数据 
	int n,m,p; 
	char opt;
	cin>>n>>m;
	cin>>num;
	
	// 执行 m 次操作
	while(m--){
		cin>>opt;
		switch(opt){
			
			// 加法操作 
			case '+':
				for(int i=n-1; i>=0;i--){
					if(num[i] == '0'){
						num[i] = '1';
						break;
					}else{
						num[i] = '0';
					}
				}
				break;
				
			// 减法操作 
			case '-':
				for(int i=n-1; i>=0; i--){
					if(num[i] == '1'){
						num[i] = '0';
						break;
					}else{
						num[i] = '1';
					}
				}
				break;
				
			// 乘法操作 
			case '*':
				num[n++] = '0';
				num[n] = '\0';
				break;
				
			// 除法操作 
			case '/':
				num[--n] = '\0';
				break;
		}
	}
	
	// 输出结果
	cout<<num; 
	
    return 0;
}



MT2182 新十六进制

难度:钻石    时间限制:1秒    占用内存:128M
题目描述

小码哥基于之前的十六进制定义了一种新的进制,其仍然由十六个基数构成(即 0,1,2, …,9,A,B,C,D,E,F),并且大小关系保持不变(即 0<1<2<…<9<A<B<C<D<E<F),唯一不同的是数字只能按照大小关系升序排列(即 11,21 这样的数是不合法的,1F 的下一个数是 23),但前导 0 会被忽略(即 00012 等于 12,也同样合法)。

现在小码哥想为这种进制制作转换器,能够把这种进制下的数转换成十进制数,但小码哥不懂怎么下手,于是他来找你帮忙。
小码哥希望对于输入的数字能进行判断,如果这个数字不合法,转换器会报错,如果合法,转换器会输出对应的十进制数。

格式

输入格式:一个字符串,为标准十六进制数。
输出格式:一个整数,表示转换成的十进制数,如果输入的十六进制数不合法输出 error

样例 1

输入:12

输出:16

备注

字符串长度小于等于 20,可能出现负数,输出文件不存在 -0。


相关知识点: 数学进位制


题解


首先对题目提出的新进制进行解读。我们来看原十六进制的前18个数:

十六进制数对应的十进制数

根据题目的意思:“新十六进制下的数字只能按照大小关系升序排列”,因此上表中的十六进制数 “10”、“11” 是不合法的,于是在新十六进制下,数 “F” 的下一个数应该为 “12”,即新十六进制数的前 18 个数为:

新十六进制数对应的十进制数

这便是题目所说的新十六进制与十进制数的对应关系。

对于输入的任意新十六进制字符串,如果要找到其对应的十进制数,我们可以通过顺序查找的方式进行求解。即从 0 开始依次枚举该数对应的十六进制数,同时再定义一个计数器 cnt 来记录当前找到的合法的新十六进制数个数,当存在某个十进制数对应的十六进制数与输入的新十六进制数一致时,计数器 cnt 保存的值即为所求。根据这样的思路可写出以下代码:

/*
	MT2182 新十六进制数(Search) 
	string.erase(string.begin())会将指定字符串的首位字符进行抹除 
*/

#include<bits/stdc++.h> 
using namespace std;


// 字符串处理:消除多余的前导0,同时反馈输入数据的正负性
int Formatting(string &str)
{
	int flag = 1;
	
	// 正负性判别 
	if(str[0] == '-'){
		flag = -1;
		str.erase(str.begin());
	}
	
	//  消除前导 0 
	while(str[0] == '0' && str.size()>1)
		str.erase(str.begin());
	
	return flag;
} 

// 字符串的合法性检测 
bool isLegal(string str)
{
	int strlen = str.size();
	if(strlen==1) return true;
	for(int i=1; i<strlen; i++)
		if(str[i] - str[i-1] <= 0)
			return false;
	return true;
}

// 根据新十六进制数的规则顺序枚举 
int getDecOfNewHex(string str)
{
	int cnt = 0; 
	string tmp;
	stringstream ss;
	for(int i=0; ; i++){
		// 将当前数字转换为 16 进制
		ss.clear();
		ss<<hex<<i;
		ss>>tmp;
		
		// 判断该十六进制数与输入字符是否一致 
		if(tmp == str) return cnt;
		
		// 若该十六进制数合法则向后计数 
		if(isLegal(tmp)) cnt++; 
	}
}

int main( )
{
	// 输入数据 
	string str;cin>>str;
	
	// 格式处理
	int flag =  Formatting(str);
	
	// 合法性检测
	if(!isLegal(str)){
		cout<<"error"<<endl;
		return 0;
	}
	
	// 顺序查找该新十六进制字符串对应的十进制数 
	cout<<flag*getDecOfNewHex(str)<<endl;
	
    return 0;
}

这份代码出现了超时情况,只能得一半的分,原因很简单:每次枚举一个十进制数都涉及到进制转换以及合法性检测两个环节(特别是合法性检测),这肯定是有超时风险的。

那要如何解决这个问题呢?其实上面的求解之所以会超时是因为我们枚举的目标集合是十进制数据集,而这个数据集中具有相当一部分数据在转换为十六进制数后为非新十六进制数,说简单点就是花了些时间枚举计算非合法的结果!这就是在做无用功。换个角度,我们可以枚举所有的新十六进制数,然后再按序为其编号不就能得到不同新十六进制数对应的十进制数了么?这里需要提一点,新十六进制数是存在最大值的(最大值显然为 123456789ABCDEF),也就是说新十六进制数是有限的,将其用树来表示得到的效果如下:

在这里插入图片描述

这为我们提供了枚举的可行性。
于是现在我们的问题是,如何枚举新十六进制数?一种简单直接的办法是用深度优先搜索算法:传递参数为上图所示根节点对应的数字,每次搜索时都从当前根节点的下一个数字进行递归搜索。这样,就能将所有合法十六进制数全部枚举出(具体代码如下)。

char chr[] = "0123456789ABCDEF";
int cnt = 1, flag = 1;
string tmp, str, s[100000]={"0"};
void dfs(int num){
    if(tmp.size()) s[cnt++] = tmp;
    for(int i=num+1; i<16; i++){
        tmp.push_back(chr[i]);
        dfs(i);
        tmp.pop_back();
    }
} 

在得到了所有新十六进制字符串后,我们只需要再对这些字符串进行排序并为其标记顺序即可(这些顺序即指示了其对应的十进制数)。而对新十六进制的排序(即 0<1<2<…<9<A<B<C<D<E<F),恰好与各字符在 ASCII 码值上的大小关系一致。但考虑到数字越长表示的数越大(这点与依赖 ASCII 码值的字符串排序不同),因此需要单独写一个用于比较新十六进制数字符串大小关系的函数,如下:

bool cmp(string a, string b){
    if(a.size() == b.size()) return a<b;
    return a.size() < b.size();
}

最后只需要通过一重循环对得到的新十六进制数集合进行扫描便能查找到指定输入对应的十进制数,下面给出基于以上思路得到的完整代码(已 AC):

/*
	MT2182 新十六进制数 
*/
#include<bits/stdc++.h> 
using namespace std;

const int MAX = 1e5+5;
char chr[] = "0123456789ABCDEF";
int cnt = 1;
string tmp, str, s[MAX]={"0"};

// 利用深搜将所有的合法数字举例出来
void dfs(int num){
	if(tmp.size()) s[cnt++] = tmp;
	for(int i=num+1; i<16; i++){
		tmp.push_back(chr[i]);
		dfs(i);
		tmp.pop_back();
	}
} 
// 字符串处理:消除多余的前导0,同时反馈输入数据的正负性
int Formatting(string &str)
{
    int flag = 1;
    // 正负性判别 
    if(str[0] == '-'){
        flag = -1;
        str.erase(str.begin());
    }
    //  消除前导 0 
    while(str[0] == '0' && str.size()>1)
        str.erase(str.begin());
    return flag;
} 
// 自定义数字比较规则 
bool cmp(string a, string b){
	if(a.size() == b.size()) return a<b;
	return a.size() < b.size();
}

int main( )
{
	// 枚举所有的新十六进制数 
	dfs(0);
	// 获取输入 
	cin>>str;
	// 对输入字符串进行处理 
	int flag = Formatting(str);
	// 排序 
	sort(s,s+cnt,cmp);
	// 查找指定新十六进制数的位置 
	for(int i=0;i<cnt;i++)
		if(s[i] == str){
			cout<<i*flag;
			return 0;
		}
	cout<<"error";
    return 0;
}


MT2172 萨卡兹人

难度:黄金    时间限制:2.5秒    占用内存:128M
题目描述

很久很久以前,卡西米尔里住着萨卡兹人,他们彼此争斗不休。有一天,他们想要研究自己的 DNA 序列,来证明他们是一个种群。首先选取一个好长好长的序列(DNA 序列包含 26 个小写英文字母),然后每次选择两个区间,这两个区间代表两个萨卡兹人的 DNA 序列,这两个萨卡兹一模一样的唯一可能是他们的 DNA 序列一模一样。

格式

输入格式:第一行输入一个DNA字符串 S;
     接下来一个数字 m m m ,表示 m m m 次询问;
     接下来 m m m 行,每行四个数字 l 1 , r 1 , l 2 , r 2 l_1,r_1, l_2,r_2 l1,r1,l2,r2 分别表示此次询问的两个区间;
     注意:字符串的位置从1开始编号。
输出格式:对于每次询问,输出一行表示结果。如果两个萨卡兹完全相同输出 Yes,否则输出 No

样例1

输入:aabbaabb
   3
   1 3 5 7
   1 3 6 8
   1 2 1 2

输出:Yes
   No
   Yes

备注

其中: 1 ≤ l 1 ≤ r 1 ≤ l e n g t h ( S ) , 1 ≤ l 2 ≤ r 2 ≤ l e n g t h ( S ) 1≤ l_1≤r_1≤length(S),1≤ l_2≤ r_2≤length(S) 1l1r1length(S),1l2r2length(S)
1 ≤ l e n g t h ( S ) , m ≤ 1000000 1≤length(S),m≤1000000 1length(S),m1000000


相关知识点:字符串


题解


这道题考察了字符串的子串判等,比较简单,下面直接给出求解的完整代码(已 AC):
/*
	MT2172 萨卡兹人 
	
*/
#include<bits/stdc++.h> 
using namespace std;

int main( )
{
	// 输入数据
	string S;
	int m,l1,r1,l2,r2,p,len;
	cin>>S>>m;
	// 字符串移位(满足题目“字符串位置从1编号”的要求) 
	S = "0"+S;
	// 执行询问
	while(m--) {
		cin>>l1>>r1>>l2>>r2;
		p=0, len = r1-l1;
		while(S[l1+p] == S[l2+p] && p<=len) p++;
		if(p > len) cout<<"Yes"<<endl;
		else cout<<"No"<<endl;
	}
    return 0;
}


MT2173 回文串等级

难度:黄金    时间限制:1秒    占用内存:128M
题目描述

所谓量变产生质变,就比如低级的材料攒多了可以合成更高级的玩意儿,高级的回文串也可以由低级的组成。任意一个字符串都可以是 0 级的回文串。而一个更高级的长为 n n n i i i 级回文串则满足其长为 ⌊ 2 n ⌋ \lfloor \frac2n \rfloor n2 (向下取整)的前后缀都为 i − 1 i-1 i1 级回文串。现在给你一个字符串,问你其每个前缀的回文串等级。

格式

输入格式:一行一个字符串S。
输出格式:|S| 个数,表示其长为 1,2,3,…,|S| 的前缀的回文串等级。

样例 1

输入:aaa

输出:1 2 2

备注

其中: 1 ≤ ∣ S ∣ ≤ 500000 1≤|S|≤500000 1S500000


相关知识点:字符串字符串哈希


题解


首先需要弄清楚题中所说回文串等级。例如对字符串 “aba”:

  • 当取前缀为 1 时(即 “a”),该字符串的长为 ⌊ 1 2 ⌋ = 0 \lfloor \frac12 \rfloor=0 21=0 的前后缀不存在(即认为前后缀回文串等级为 0),且字符串 “a” 本身是一个回文串,因此认为 “a” 的回文串等级为 0+1=1(实际上,可以认为长度为 1 的字符的回文串等级就为 1);
  • 当取前缀为 2 时(即 “ab”),该字符串的长为 ⌊ 2 2 ⌋ = 1 \lfloor \frac22 \rfloor=1 22=1 的前后缀分别为 “a” 和 “b”(且各自的回文串等级均为 1),但字符串 “ab” 本身不是一个回文串,因此 “ab” 的回文串等级为 0;
  • 当取前缀为 3 时(即 “aba”),该字符串的 ⌊ 3 2 ⌋ = 1 \lfloor \frac32 \rfloor=1 23=1 的前后缀子串均为 “a”,都是 1 级回文串,因此 “aba” 的回文串等级为 1+1=2;

根据这样的思路,可写出以下代码:

// 输入数据(通过移位限定输入字符串的起始地址从1开始)
cin>>(s+1); 
len = strlen(s+1);
// 初始化:第1个前缀的回文串等级必定是1
ans[1]=1;
cout<<1<<" ";
// 遍历原字符串的所有前缀,并求出各前缀的回文串等级
for(int i=2;i<=len;i++){
   if(isPlalindrome(s,1,i))
     ans[i] = ans[i>>1] + 1;
     cout<<ans[i]<<" ";
}

其中,函数 isPlalindrome(char *s, int start, int end) 用于判断字符串 s 从 start 到 end 的子串是否为回文。

该代码完整描述了求解本题的整体思路,但存在严重的超时风险。因为每次扫描指定字符串的所有前缀时,都需要单独判断此前缀串是否为回文,这在题目所给的数据范围内是必定超时的。因此,为了能完整通过全部测试数据,就必须想办法解决 “判断指定字符串的所有前缀串是否为回文” 这一难题。

这便需要用到字符串哈希。我们知道,回文串是正着和倒着读结果都不变的字符串,基于这种性质,假设现在有一个长为 n n n 的回文字符串 S S S。如果我们随机取一个数 p p p,则总存在:

S 1 p n − 1 + S 2 p n − 2 + ⋯ + S n = S 1 + S 2 p + ⋯ + S n p n − 1 S_1 p^{n-1}+S_2 p^{n-2}+⋯+S_n=S_1+S_2 p+⋯+S_n p^{n-1} S1pn1+S2pn2++Sn=S1+S2p++Snpn1

其中: S i S_i Si 表示字符串 S S S 中的第 i i i 个字符对应的 ASCII 值。例如,数字回文串 “11311” 始终满足:

1 p 4 + 1 p 3 + 3 p 2 + 1 p + 1 = 1 + 1 p + 3 p 2 + 1 p 3 + 1 p 4 1p^4+1p^3+3p^2+1p+1=1+1p+3p^2+1p^3+1p^4 1p4+1p3+3p2+1p+1=1+1p+3p2+1p3+1p4

根据该等式,我们可以通过计算一个字符串的前缀和与后缀和数组来快速地判断该字符串中任意前缀是否为一个回文串。例如,对于数字字符串 “112113”,我们可以维护以下两个数组(取 p = 5 p=5 p=5):

在这里插入图片描述

基于这两个数组,我们能在常数时间内判断出字符串 “112113” 的任意前缀子串是否为回文串:

  • 长度为 1 的前缀 “1”:数组 H 1 H_1 H1 记录的该子串的前缀和为 H 1 [ 1 ] = 1 H_1 [1]=1 H1[1]=1,数组 H 2 H_2 H2 记录的该子串的前缀和为 H 2 [ 1 ] − H 2 [ 1 + 1 ] ∗ 5 = 1 H_2 [1]-H_2 [1+1]*5=1 H2[1]H2[1+1]5=1,因此有 P r e H 1 = P r e H 2 PreH_1=PreH_2 PreH1=PreH2,所以前缀 “1” 是一个回文串;
  • 长度为 2 的前缀 “11”:数组 H 1 H_1 H1 记录的该子串的前缀和为 H 1 [ 2 ] = 6 H_1 [2]=6 H1[2]=6,数组 H 2 H_2 H2 记录的该子串的前缀和为 H 2 [ 1 ] − H 2 [ 2 + 1 ] ∗ 5 2 = 6 H_2 [1]-H_2 [2+1]*5^2=6 H2[1]H2[2+1]52=6,因此有 P r e H 1 = P r e H 2 PreH_1=PreH_2 PreH1=PreH2,所以前缀 “11” 是一个回文串;
  • 长度为 3 的前缀 “112”:数组 H 1 H_1 H1 记录的该子串的前缀和为 H 1 [ 3 ] = 32 H_1 [3]=32 H1[3]=32,数组 H 2 H_2 H2 记录的该子串的前缀和为 H 2 [ 1 ] − H 2 [ 3 + 1 ] ∗ 5 3 = 56 H_2 [1]-H_2 [3+1]*5^3=56 H2[1]H2[3+1]53=56,因此有 P r e H 1 ≠ P r e H 2 PreH_1\neq PreH_2 PreH1=PreH2,所以前缀 “112” 不是一个回文串;
  • 长度为 4 的前缀 “1121”:数组 H 1 H_1 H1 记录的该子串的前缀和为 H 1 [ 4 ] = 161 H_1 [4]=161 H1[4]=161,数组 H 2 H_2 H2 记录的该子串的前缀和为 H 2 [ 1 ] − H 2 [ 4 + 1 ] ∗ 5 4 = 206 H_2 [1]-H_2 [4+1]*5^4=206 H2[1]H2[4+1]54=206,因此有 P r e H 1 ≠ P r e H 2 PreH_1\neq PreH_2 PreH1=PreH2,所以前缀 “1121” 不是一个回文串;
  • 长度为 5 的前缀 “11211”:数组 H 1 H_1 H1 记录的该子串的前缀和为 H 1 [ 5 ] = 1 × 5 4 + 1 × 5 3 + 2 × 5 2 + 1 × 5 + 1 × 1 H_1 [5]=1×5^4+1×5^3+2×5^2+1×5+1×1 H1[5]=1×54+1×53+2×52+1×5+1×1,数组 H 2 H_2 H2 记录的该子串的前缀和为 H 2 [ 1 ] − H 2 [ 5 + 1 ] ∗ 5 5 = 1 × 1 + 1 × 5 + 2 × 5 2 + 1 × 5 3 + 1 × 5 4 H_2 [1]-H_2 [5+1]*5^5=1×1+1×5+2×5^2+1×5^3+1×5^4 H2[1]H2[5+1]55=1×1+1×5+2×52+1×53+1×54,显然 P r e H 1 = P r e H 2 PreH_1= PreH_2 PreH1=PreH2,所以前缀 “11211” 是一个回文串;
  • 长度为 6 的前缀 “112113”:数组 H 1 H_1 H1 记录的该子串的前缀和为 H 1 [ 6 ] = 1 × 5 5 + 1 × 5 4 + 2 × 5 3 + 1 × 5 2 + 1 × 5 + 3 × 1 H_1 [6]=1×5^5+1×5^4+2×5^3+1×5^2+1×5+3×1 H1[6]=1×55+1×54+2×53+1×52+1×5+3×1,数组 $H_2 $ 记录的该子串的前缀和为 H 2 [ 1 ] = 1 × 1 + 1 × 5 + 2 × 5 2 + 1 × 5 3 + 1 × 5 4 + 3 × 5 5 H_2 [1]=1×1+1×5+2×5^2+1×5^3+1×5^4+3×5^5 H2[1]=1×1+1×5+2×52+1×53+1×54+3×55,显然 P r e H 1 ≠ P r e H 2 PreH_1\neq PreH_2 PreH1=PreH2,所以前缀 “112113” 不是一个回文串。

这便是利用字符串哈希判断任意字符串的所有前缀子串是否为回文的方法。下面给出基于该思路的完整代码(已 AC):

/*
	MT2173 回文串等级 
	哈希求解 
*/
#include<bits/stdc++.h> 
using namespace std;

// 定义一个较大的质数 
const int Z = 111;
const int MAX = 5e5+7;
unsigned long long p[MAX], h1[MAX], h2[MAX];
char s[MAX];
int len, ans[MAX];

// 预处理哈希函数的前缀和 
void init(int n){
	p[0]=1;
	for(int i=1; i<=n; i++){
		p[i] = p[i-1]*Z;
		h1[i] = h1[i-1]*Z + s[i];
		h2[n-i+1] = h2[n-i+2]*Z + s[n-i+1];
	}
}

int main( )
{
	// 输入数据(通过移位限定输入字符串的起始地址从1开始)
    cin>>s+1; 
    len = strlen(s+1);
	init(len);
	// 初始化:第1个前缀的回文串等级必定是1
    ans[1]=1;
	cout<<1<<" ";
	// 遍历原字符串的所有前缀,并求出各前缀的回文串等级
    for(int i=2;i<=len;i++){
        // 判断当前的前缀子串是否是回文串
        if(h1[i] == h2[1]-h2[i+1]*p[i])
            ans[i] = ans[i>>1] + 1;
        cout<<ans[i]<<" ";
    }
    return 0;
}


MT2175 五彩斑斓的串

难度:钻石    时间限制:1秒    占用内存:128M
题目描述

小码哥是一个喜欢字符串的男孩子。
小码哥在研究字典序的性质。他发现他可以通过改变字母表的顺序来改变两个字符串的大小关系。
例如,通过将现有的字母表顺序 “abcdefghijklmnopqrstuvwxyz” 改为 “abcdefghijklonmpqrstuvwxyz”,字符串 “omm” 会小于 “mom”。
现在小码哥有 n n n 个字符串,对于其中的一个字符串,如果存在某种字母表顺序,使得它在这 n n n 个字符串中字典序最小,那这个字符串就是一个五彩斑斓的串。
现在小码哥想找出所有五彩斑斓的串,由于小码哥忙于他的研究,找出所有五彩斑斓的串的重任被交到了你的身上。

格式

输入格式:第一行一个正整数 n n n,表示字符串的个数;
     接下来 n n n 行,每行一个字符串。
输出格式:输出第一行为一个正整数,为五彩斑斓的串的个数;
     接下来对于每个五彩斑斓的串输出一行。
     注意:输出的串的顺序应该和输入一致!

样例 1

输入:4
   omm
   moo
   mom
   ommnom

输出:2
   omm
   mom

备注

测试数据保证 1 ≤ n ≤ 3 × 1 0 4 1≤n≤3×10^4 1n3×104 ,所有字符串的总长不超过 3 × 1 0 5 3×10^5 3×105,且字符集为小写英文字母。


相关知识点:字符串字典树


题解


题目的意思是对输入的一系列字符串,判断其中的每个字符串是否存在一种字典序使得其在这些字符串集中最小。例如,对于题目给出的字符串集 {omm, moo, mom, ommnom}:

  • 字符串 “omm” 在给定满足 “o<m” 的字典序中总存在:omm < ommnom < moo < mom,此时字符串 “omm” 是最小的。于是称字符串 “omm” 是五彩斑斓的串;
  • 字符串 “moo” 无论是在给定满足 “o<m” 的字典序(此时有omm < ommnom < moo < mom)中,还是在给定满足 “m<o” 的字典序(此时有 mom < moo < omm < ommnom)中,字符串 “omm” 均不是最小的。于是认为字符串 “moo” 不是五彩斑斓的串;
  • 字符串 “mom” 在给定满足 “m<o” 的字典序中总存在:mom <moo < omm < ommnom,此时字符串 “mom” 是最小的。于是称字符串 “mom” 是五彩斑斓的串;
  • 字符串 “ommnom” 在给定的字符串集中,存在它的前缀字符串 “omm”,因此无论在怎样的字典序下,字符串 “ommnom” 总是比 “omm” 大,因此字符串 “ommnom” 不可能是五彩斑斓的串。

为了判断一个字符串是否为 “五彩斑斓的串”,最直接的办法就是枚举全部的字典序,并求出在各字典序下的最小的字符串。但这样的求解在极端情况下(即给定的字符串集中含有全 26 个字符),将存在 26! 个字典序,这肯定超时无疑!此时要如何求解这个问题呢?这便需要用到字典树。

字典树是一种可高效地存储和检索字符串的树形数据结构,例如,对于题目给出的字符串集 {omm, moo, mom, ommnom},可构建如下字典树:

在这里插入图片描述

需要说明的是,字典树并没有严格的字母表顺序,他只是一种存储结构。所以,在上面的字典树中,各分支之间的位置关系并不代表这些字符之间的顺序,他们彼此之间的地位对等。

为了找到本题所说的 “五彩斑斓的串”,我们必须单独构建一个字母之间大小关系的表。为此,定义一个规格为 26×26 的矩阵 m,它的含义如下:

  • m [ i ] [ j ] = 1 m[i][j] = 1 m[i][j]=1:表示字符 i i i 的字典序大于字符 j j j
  • m [ i ] [ j ] = − 1 m[i][j] = -1 m[i][j]=1:表示字符 i i i 的字典序小于字符 j j j

根据这个矩阵,以及字典树,我们便能快速的判断出一个字符是否能成为一个 “五彩斑斓的串”。以题目给出的字符串集为例,下面阐述其具体的判别过程:

  • 字符串 “omm”:首先初始化矩阵 m 中的各取值为 0。接下来来到字典树的第一层,该层有两个分支,且对应节点中存放的字符分别为 “o” 和 “m”。为了使得字符串 “omm” 最小,就必须要求字母 “o” 的字典序小于 “m”,于是置 m[o][m] = -1, m[m][o] = 1。接下来沿着字符串 “omm” 的分支继续往下,来到第二层,该层仅有一个分支,因此无需添加字典序规则。继续往下,来到第三层,该层依然只有一个分支,因此无需添加字典序规则。字符串 “omm” 扫描结束,从矩阵 m 中的结果可知,要使字符串 “omm” 成为 “五彩斑斓的串” 的字典序为 “o<m”。
  • 字符串 “moo”:首先初始化矩阵 m 中的各取值为 0。接下来来到字典树的第一层,该层有两个分支,且对应节点中存放的字符分别为 “o” 和 “m”。为了使得字符串 “moo” 最小,就必须要求字母 “m” 的字典序小于 “o”,于是置 m[m][o] = -1, m[o][m] = 1。接下来沿着字符串 “moo” 的分支继续往下,来到第二层,该层仅有一个分支,因此无需添加字典序规则。继续往下,来到第三层,该层有两个分支,且对应节点中存放的字符分别为 “o” 和 “m”。为了使得字符串 “moo” 最小,就必须要求字母 “o” 的字典序小于 “m”,但是现存 m 矩阵中,已经存在 m[m][o] = -1, m[o][m] = 1。这与现在的要求相矛盾,因此认为字符串 “moo” 不可能成为 “五彩斑斓的串”。
  • 字符串 “mom”:首先初始化矩阵 m 中的各取值为 0。接下来来到字典树的第一层,该层有两个分支,且对应节点中存放的字符分别为 “o” 和 “m”。为了使得字符串 “mom” 最小,就必须要求字母 “m” 的字典序小于 “o”,于是置 m[m][o] = -1, m[o][m] = 1。接下来沿着字符串 “omm” 的分支继续往下,来到第二层,该层仅有一个分支,因此无需添加字典序规则。继续往下,来到第三层,该层有两个分支,且对应节点中存放的字符分别为 “o” 和 “m”。为了使得字符串 “mom” 最小,就必须要求字母 “m” 的字典序小于 “o”,这与现存 m 矩阵中的规则一致,故无需更新。字符串 “mom” 扫描结束,从矩阵 m 中的结果可知,要使字符串 “mom” 成为 “五彩斑斓的串” 的字典序为 “m<o”。
  • 字符串 “ommnom”:在给定的字符串集中,存在它的前缀字符串 “omm”,因此对 m 矩阵进行更新时,其前半段的更新与对字符串 “omm” 的更新一致。接下来当程序继续往下扫描时,它将读取到一个字符结束标记,即系统将识别到字符串 “ommnom” 在给定字符串集中存在前缀,因此直接退出程序,并反馈字符串 “ommnom” 不可能是五彩斑斓的串。

此外,需要注意一点,字母之间的大小关系是可传递的。例如,假设现在得到了字母顺序 “a<b<c”。接下来,当我们得到一个新的顺序 “c<d” 时,除了要更新 m[c][d] 和 m[d][c],还需要将所有原先就比 “c” 小的字母统一规定小于 “d”,即需要更新 m[a][d]、m[d][a]、m[b][d] 和 m[d][b];所有原先就比 “d” 大的字母统一规定大于 “c”。即,要充分考虑 m 矩阵中的所有现存规则,并实时地动态更新。

下面给出基于以上思路写出的完整代码(已 AC):

/*
	MT2175 五彩斑斓的串 
*/
#include<bits/stdc++.h> 
using namespace std;

const int N = 3e5+5;
int n, ans[N], cnt;
string s[N];
char tmp[N];

struct Trie{
	// m 矩阵指示了各字符的大小关系(从而形成一个字典序) 
	// m[i][j] = 1:表示字符 i 的字典序大于字符 j 
	// m[i][j] = -1:表示字符 i 的字典序小于字符 j 
	// A 数组存放所有比指定字符小的字符;B 数组存放所有比指定字符大的字符 
	int id, nex[N][26], m[26][26], A[30], B[30];
	
	// 字符串的结束标记 
	bool end[N];
	
	// 插入字符串 
	void insert(string s){
		int p = 0, l=s.size(), c;
		// 构建字符串中各字符在字典树中的位置关系 
		for(int i=0; i<l; i++){
			c = s[i] - 'a';
			if(!nex[p][c]) nex[p][c] = ++id;
			p = nex[p][c];
		}
		end[p]=1; 
	}
	
	// 判断给定字符串是否存在一个能使其最小的字典序 
	bool check(string s){
		// 每次执行判断时必须重置字符的大小关系矩阵 
		memset(m, 0, sizeof(m));
		int p=0, l = s.size();
		for(int i=0; i<l; i++){
			int c = s[i] - 'a';
			// 1. 查看是否有已知的更短的前缀字符串 
			if(end[p]) return false;
			// 2. 查看是否有兄弟之间的关系 
			for(int j=0; j<26; j++)
				if(nex[p][j] && m[c][j]>0 && j!=c)
					return false;
			// 3. 更新前后序的节点组合
			A[0] = B[0] = 0;
			for(int j=0; j<26; j++) {
				// 如果存在兄弟节点,要使当前字符串为“五彩斑斓的串”(即当前字符在字典序里最小) 
				// 就必须使得字符 c 小于 字符 j,即 m[c][j] = -1 
				if(nex[p][j] && j!=c){
					m[c][j] = -1;
					m[j][c] = 1;
					// 接下来必须将字符 c 的大小关系传递至整个已有的字典序 m 中 
					for(int k=0; k<26; k++){
						// 维护前序字符集的规则:所有本来就比 c 小的字符也应该小于 j 
						if(m[c][k] == 1){
							// 将所有比字符 c 小的字符记录进入 A 数组,为了节省计数变量,这里就用 A[0] 
							A[++A[0]] = k;
							m[j][k] = 1;
							m[k][j] = -1;
						}
						// 维护后序字符集的规则:所有本来就比 j 大的字符也应该大于 c 
						if(m[j][k] == -1){
							// 将所有比字符 c 大的字符记录进入 B 数组,为了节省计数变量,这里就用 B[0] 
							B[++B[0]] = k;
							m[c][k] = -1;
							m[k][c] = 1;
						}
					}
				}
			}
			// 	建立字典关系:A 数组中的所有字符自然是小于 B 数组中的所有字符 
			for(int j=1; j<=A[0]; j++)
				for(int k=1; k<=B[0]; k++){
					m[A[j]][B[k]] = -1;
					m[B[k]][A[j]] = 1;
				}
			p = nex[p][c];
		}
		return true;
	}
};
Trie trie;


int main( )
{
	// 获取输入
	cin>>n;
	// 插入所有的字符串,构建字典树
	for(int i=1; i<=n; i++){
		cin>>s[i];
		trie.insert(s[i]);
	}
	// 依次扫描每个字符串,检测其是否能成为“五彩斑斓的串”
	for(int i=1; i<=n; i++){
		if(trie.check(s[i]))
			ans[++cnt] = i;
	}
	// 输出
	cout<<cnt<<endl;
	for(int i=1; i<=cnt; i++)
		cout<<s[ans[i]]<<endl;
	return 0;
}

END


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/872643.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DataGrip 安装 与 连接MySQL数据库

DataGrip 安装 与 连接MySQL数据库 Jetbrains是著名的编程工具商业软件提供商&#xff0c;旗下有很多软件。包括IDE、团队开发工具、插件和微软.Net辅助工具、包括自创语言Kotlin等。我们通常用的和说的全家桶&#xff0c;主要就是指它的IDE套件。Jetbrains的IDE工具都支持跨平…

web-Element

在vueapp里<div><!-- <h1>{{message}}</h1> --><element-view></element-view></div> <div><!-- <h1>{{message}}</h1> --><element-view></element-view></div>在view新建个文件 <t…

AIGC+游戏:一个被忽视的长赛道

&#xff08;图片来源&#xff1a;Pixels&#xff09; AIGC彻底变革了游戏&#xff0c;但还不够。 数科星球原创 作者丨苑晶 编辑丨大兔 消费还没彻底复苏&#xff0c;游戏却已经出现拐点。 在游戏热度猛增的背后&#xff0c;除了版号的利好因素外&#xff0c;AIGC技术的广泛…

项目实战 — 消息队列(8){网络通信设计②}

目录 一、客户端设计 &#x1f345; 1、设计三个核心类 &#x1f345; 2、完善Connection类 &#x1f384; 读取请求和响应、创建channel &#x1f384; 添加扫描线程 &#x1f384; 处理不同的响应 &#x1f384; 关闭连接 &#x1f345; 3、完善Channel类 &#x1f384; 编…

机器学习编译系列

机器学习编译MLC 1. 引言2. 机器学习编译--概述2.1 什么是机器学习编译 1. 引言 陈天奇目前任教于CMU&#xff0c;研究方向为机器学习系统。他是TVM、MXNET、XGBoost的主要作者。2022年夏天&#xff0c;陈天奇在B站开设了《机器学习编译》的课程。   《机器学习编译》课程共分…

2023最新水果编曲软件FL Studio 21.1.0.3267音频工作站电脑参考配置单及系统配置要求

音乐在人们心中的地位日益增高&#xff0c;近几年音乐选秀的节目更是层出不穷&#xff0c;喜爱音乐&#xff0c;创作音乐的朋友们也是越来越多&#xff0c;音乐的类型有很多&#xff0c;好比古典&#xff0c;流行&#xff0c;摇滚等等。对新手友好程度基本上在首位&#xff0c;…

全网最牛,Appium自动化测试框架-关键字驱动+数据驱动实战(一)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、关键字驱动框架…

Stm32-使用TB6612驱动电机及编码器测速

这里写目录标题 起因一、电机及编码器的参数二、硬件三、接线四、驱动电机1、TB6612电机驱动2、定时器的PWM模式驱动电机 五、编码器测速1、定时器的编码器接口模式2、定时器编码器模式测速的原理3、编码器模式的配置4、编码器模式相关代码5、测速方法 六、相关问题以及解答1、…

关于Cesium的常见需求整理之点位和弹窗(点位弹窗)

一、点位上图 ①在Cesium中&#xff0c;每个自定义的地图元素被视为一个entity对象&#xff0c;如果我们要添加点位到地图上&#xff0c;那就必须先创建一个entity对象。 var entity new Cesium.Entity({position: position, });以上代码我们创建了一个entity对象&#xff0…

Autosar通信入门系列06-聊聊CAN通信的线与机制与ACK应答

本文框架 1. 概述2. CAN通信的线与机制3. ACK应答机制理解 1. 概述 本文为Autosar通信入门系列介绍&#xff0c;如您对AutosarMCAL配置&#xff0c;通信&#xff0c;诊断等实战有更高需求&#xff0c;可以参见AutoSar 实战进阶系列专栏&#xff0c;快速链接&#xff1a;AutoSa…

数据库基础(增删改查)

目录 MySQL 背景知识 数据库基础操作 1.创建数据库 2.查看所有数据库 3.选中指定的数据库 4.删除数据库 数据库表操作 MySQL的数据类型 1.创建表 3.查看指定表的结构 4.删除表 增删改 新增操作 修改(Updata) 删除语句 面试题 查询操作 指定列查询 查询的列为表达式…

系统设计:通用思路之4S分析法

1.系统设计 系统设计是一个定义系统架构、功能模块、服务及接口和数据存储等满足特定需求的过程。 与面向对象设计不同的是&#xff0c;面向对象设计通常是对于某个特定功能模块的设计&#xff0c;通常要求设计类图关系、接口关系、实现关系等涉及具体代码层面的设计&#xff…

C语言库函数之 qsort 讲解、使用及模拟实现

引入 我们在学习排序的时候&#xff0c;第一个接触到的应该都是冒泡排序&#xff0c;我们先来复习一下冒泡排序的代码&#xff0c;来作为一个铺垫和引入。 代码如下&#xff1a; #include<stdio.h>void bubble_sort(int *arr, int sz) {int i 0;for (i 0; i < sz…

基于chatgpt动手实现一个ai_translator

动手实现一个ai翻译 前言 最近在极客时间学习《AI 大模型应用开发实战营》&#xff0c;自己一边跟着学一边开发了一个进阶版本的 OpenAI-Translator&#xff0c;在这里简单记录下开发过程和心得体会&#xff0c;供有兴趣的同学参考&#xff1b; ai翻译程序 版本迭代 在学习…

C语言必会题目(2)

W...Y的主页 &#x1f60a; 代码仓库分享&#x1f495; 今天继续分享C语言必会的题目&#xff0c;上一篇文章主要是一些选择题&#xff0c;而今天我们主要内容为编程题的推荐与讲解 准备好迎接下面的题了吗&#xff1f;开始发车了&#xff01;&#xff01;&#xff01; 输入…

pytest运行时参数说明,pytest详解,pytest.ini详解

一、Pytest简介 1.pytest是一个非常成熟的全功能的Python测试框架&#xff0c;主要有一下几个特点&#xff1a; 简单灵活&#xff0c;容易上手&#xff0c;支持参数化 2.能够支持简单的单元测试和复杂的功能测试&#xff0c;还可以用来做selenium、appium等自动化测试&#xf…

zookeeper案例

目录 案例一&#xff1a;服务器动态上下线 服务端&#xff1a; &#xff08;1&#xff09;先获取zookeeper连接 &#xff08;2&#xff09;注册服务器到zookeeper集群&#xff1a; &#xff08;3&#xff09;业务逻辑&#xff08;睡眠&#xff09;&#xff1a; 服务端代码…

提高生产力 | Apifox 数据结构验证最佳实践

目录 实践场景 定义返回响应 场景数据准备 校验响应数据 总结 在设计接口的过程中&#xff0c;响应数据需要和返回响应规范一一对应。这样能够确保接口的一致性和可靠性&#xff0c;并且方便接口的使用和维护&#xff0c;即使在后续迭代过程中出现问题&#xff0c;开发人员…

zabbix监控安装部署

目录 一、环境 二、配置 1.配置yum源&#xff0c;这里用的清华的 2.过滤一下安装包&#xff0c;查看依赖包 安装依赖包 3.配置数据库 开机自启 创建数据库 创建用户 授权 导入数据到数据库 查看zabbix数据库有没有表和数据 4.修改zabbix配置文件 1.修改zabbix配置…

【Java】常见面试题:多线程

文章目录 1. 谈谈进程和线程之间的区别【高频】2. java中有哪些方式来创建线程&#xff1f;3. run和start的区别【经典面试题】4. Java线程的状态5. 【线程不安全的原因】6. 就以count为例&#xff1a;一个线程加锁、一个线程不加锁&#xff0c;此时能否保证线程的安全呢&#…