《算法竞赛·快冲300题》将于2024年出版,是《算法竞赛》的辅助练习册。
所有题目放在自建的OJ New Online Judge。
用C/C++、Java、Python三种语言给出代码,以中低档题为主,适合入门、进阶。
“ 最短缺失子序列” ,链接: http://oj.ecustacm.cn/problem.php?id=1829
题目描述
【题目描述】 字符串t是字符串s的子序列:字符串s删除0个或者多个字符可以变成字符t。
注意:t是s的子序列,t在s中不一定是连续的,只要t中的字符出现的顺序与s相同即可。
例如s=“abcd”,t=“ad”,此时t是s的子序列。
字符串t是字符串s的缺失子序列:字符串t不是字符串s的子序列,但是字符串s和t中出现的字母,均在集合v中出现过(题目存在修改)。
例如s=“abcd”,t=“bac”,此时t是s的缺失子序列。
字符串t是字符串s的最短缺失子序列:字符串t是字符串s的缺失子序列,同时长度是最短的。
例如s=“abcd”,t=“aa”,此时t是s的最短缺失子序列,"ba"也是s的最短缺失子序列。
现在给定字符串s,询问m次,每次询问一个字符串t是否为s的最短缺失子序列。
【输入格式】 第一行为给定的小写字母字符集v,长度为[1,26],每个字符仅出现一次。
之后所有输入的字符串中的字母均属于v。
第二行为字符串s,1≤|s|≤1000000。
第三行为正整数m,表示询问次数,,1≤m≤1000000。
接下来m行,每行一个字符串t,表示每次的询问字符串,,1≤|t|≤1000000。
输入保证所有询问字符串长度之和不超过1000000.
【输出格式】 对于每次询问,如果字符串t是字符串s的最短缺失子序列,则输出1,否则输出0。
【输入样例】
abc
abcccabac
3
cbb
cbba
cba
【输出样例】
1
0
0
题解
本题需要依次解决2个问题:
(1)s的最短缺失子序列的长度len等于几?
(2)若t的长度等于len,它是不是s的最短缺失子序列?
第(1)个问题,求最短缺失子序列的长度len,下面推理计算过程。以v = “abc”,s = “abbacccabac”为例,从左到右检查s的字符。v中共有K = 3个字符。s的下标从1开始,即第一个字符是s[1]=‘a’。
len初值为1。
第一轮检查,检查到s[i]时,若s[1] ~ s[i]正好包含了所有K个字符,那么len = 2。因为此时不存在长度为1的最短缺失子序列,而存在长度为2的最短缺失子序列。例如检查到s[1] ~ s[5] = ”abbac”时,最后的”c”第一次出现。长度为1的子序列共有3个,是{‘a’, ‘b’, ‘c’},它们在s[1] ~ s[5]中存在。长度为2的最短缺失子序列,例如“ca”,它在”abbac”中不存在。
第二轮检查,检查到s[j]时,若s[i+1] ~ s[j]中正好再次包含了所有K个字符,那么len = 3。因为此时不存在长度为2的最短缺失子序列,而存在长度为3的最短缺失子序列。
长度为2的子序列有3×3=9个,是{aa, bb, cc, ab, ac, ba, bc, ca, cb},其中第一个字符可以在第一轮的s[1] ~ s[i]中找到,第二个字符可以在第二轮的s[i+1] ~ s[j]中找到。注意在第二轮的字符中,最后的s[j]在这一轮中第一次出现。
至于长度为3的最短缺失子序列,可以这样构造:取第一轮的最后字符s[i],和第二轮的最后字符s[j],再加一个字符,就是一个长度为3的最短缺失子序列。这样构造的正确性简单说明如下:设s的前2轮字符是“***c***b”,其中“***c”是第一轮,c是最后且唯一的,“***b”是第二轮,“b”是最后且唯一的,很容易证明,“cb*”不可能在“***c***b”中出现,它是最短缺失子序列。例如检查到s[1] ~ s[9] = “abbac-ccab”时,长度为3的最短缺失子序列有“cba”、“cbb”、“cbc”等。不过,这样构造出的最短缺失子序列并不包含所有的,例如“caa”也是最短缺失子序列,但它不在构造的3个序列里面。
经过多轮检查,就得到了len,它等于轮次+1。
编码时,如何判断每一轮的字符中是否包含所有v的字符?这里简单地用二进制来处理。定义vK,它的二进制中每个’1’代表存在v中存在的字符,例如v = ”abc”,则vK = …000111,’a’对应最后一个’1’,’b’对应第二个’1’,’c’对应第三个’1’。同样,每一轮中s中存在的字符用sK的二进制表示。若vK = sK,那么这一轮中s的字符包含了v的所有字符。
求len的过程是贪心。
第(2)个问题,长度为len的字符串t,是s的最短缺失子序列吗?
先考虑暴力法,一个个地查找t中的字符是否在s中:第一个字符t[1],设在s[i]处第一次找到t[1];第二个字符t[2],继续从s[i+1]开始找,设在s[j]处找到;…直到检查完t的所有字符是否在s中。对一个t做一次询问的计算量是O(n)的;做m次询问,总计算量O(mn),超时。
如果预计算出s[i]后面每个字符第一次出现的位置,那么就能快速查找了。定义Next[i][j],表示s[i]之后,第j种字符第一次出现的位置。例如s = “abbacccabac”,这里下标从1开始,即第一个字符是s[1] = ’a’。Next[0][0] = Next[0][‘a’-’a’] = 1是第0种字符’a’第一次出现的位置,位于s[1] = ’a’处;Next[0][1] = Next[0][‘b’-’a’] = 2是第1种字符’b’第一次出现的位置,位于s[2] = ’b’处;Next[5][1] = Next[5][‘b’-’a’] = 9是字符’b’在s[5]后第一次出现的位置,等等。
有了Next[][],在s中暴力查找t时就快了。先查第一个字符t[1],就是查pos = Next[0][t[1]-’a’];再查第二个字符t[2],就是查pos = Next[pos][t[1]-’a’];等等。如果有一次查询时pos = 0,说明没有找到,返回1。
【重点】 。
C++代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
char v[30], s[N], t[N];
int Next[N][26]; //Next[i][j]: S[i]后面字符 'a'+j 的位置
int main(){
scanf("%s", v + 1); //从v[1]开始存
scanf("%s", s + 1);
int vlen = strlen(v + 1), slen = strlen(s + 1); //不能写成strlen(v)-1,因为v[0]是0,空
//下面先求最短缺失子序列长度len
int vK = 0, len = 1;
for(int i = 1; i <= vlen; i++)
vK |= (1 << (v[i] - 'a')); //vK的二进制: 记录v有哪些字符
int sK = 0;
for(int i = 1; i <= slen; i++){
sK |= (1 << (s[i] - 'a')); //sK的二进制: 记录s有哪些字符
if(sK == vK) len++, sK = 0; //
//对于字符s[i],往前暴力更新Next数组
for(int j = i - 1; j >= 0; j--){
Next[j][s[i] - 'a'] = i;
if(s[j] == s[i]) break; //直到找到上一个s[i]停止
}
}
//下面判断t是否为缺失子序列
int n; scanf("%d", &n);
while(n--){
scanf("%s", t + 1);
int tlen = strlen(t + 1);
int ok = 0;
if(tlen == len ) { //t的长度等于len
int pos = 0;
for(int i = 1; i <= tlen; i++) {
pos = Next[pos][t[i] - 'a'];
if(!pos) break;
}
ok = (pos == 0); //pos等于0说明无法匹配,此时为缺失子序列
}
printf("%d\n", ok);
}
return 0;
}
Java代码
import java.util.*;
import java.io.*;
public class Main {
static final int N = 1_000_010;
static char[] v = new char[30];
static char[] s = new char[N];
static char[] t = new char[N];
static int[][] Next = new int[N][26]; // Next[i][j]: S[i]后面字符 'a'+j 的位置
public static void main(String[] args) throws IOException{
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
String str;
str = reader.readLine();
for (int i = 0; i < str.length(); i++) v[i + 1] = str.charAt(i);
int vlen = str.length();
str = reader.readLine();
for (int i = 0; i < str.length(); i++) s[i + 1] = str.charAt(i);
int slen = str.length();
// 下面先求最短缺失子序列长度len
int vK = 0, len = 1;
for (int i = 1; i <= vlen; i++)
vK |= (1 << (v[i] - 'a')); // vK的二进制: 记录v有哪些字符
int sK = 0;
for (int i = 1; i <= slen; i++) {
sK |= (1 << (s[i] - 'a')); // sK的二进制: 记录s有哪些字符
if (sK == vK){len++;sK = 0;}
// 对于字符s[i],往前暴力更新Next数组
for (int j = i - 1; j >= 0; j--) {
Next[j][s[i] - 'a'] = i;
if (s[j] == s[i]) break; // 直到找到上一个s[i]停止
}
}
// 下面判断t是否为缺失子序列
int n = Integer.parseInt(reader.readLine());
while (n-- > 0) {
str = reader.readLine();
int tlen = str.length();
for (int i = 0; i < str.length(); i++) t[i + 1] = str.charAt(i);
int ok = 0;
if (tlen == len) { // t的长度等于len
int pos = 0;
for (int i = 1; i <= tlen; i++) {
pos = Next[pos][t[i] - 'a'];
if (pos == 0) break;
}
if(pos==0) ok=1;// pos等于0说明无法匹配,此时为缺失子序列
}
writer.write(Integer.toString(ok));
writer.newLine();
}
reader.close();
writer.flush();
writer.close();
}
}
Python代码
v = [''] * 30
s = [''] * 1000010
t = [''] * 1000010
Next = [[0] * 26 for _ in range(1000010)] # Next[i][j]: S[i]后面字符 'a'+j 的位置
v[1:] = input().strip()
s[1:] = input().strip()
vlen, slen = len(v) - 1, len(s) - 1
# 下面先求最短缺失子序列长度len
vK, len_ = 0, 1
for i in range(1, vlen + 1):
vK |= (1 << (ord(v[i]) - ord('a'))) # vK的二进制: 记录v有哪些字符
sK = 0
for i in range(1, slen + 1):
sK |= (1 << (ord(s[i]) - ord('a'))) # sK的二进制: 记录s有哪些字符
if sK == vK:
len_ += 1
sK = 0
# 对于字符s[i],往前暴力更新Next数组
for j in range(i - 1, -1, -1):
Next[j][ord(s[i]) - ord('a')] = i
if s[j] == s[i]: break # 直到找到上一个s[i]停止
# 下面判断t是否为缺失子序列
n = int(input())
for _ in range(n):
t[1:] = input().strip()
tlen = len(t) - 1
ok = 0
if tlen == len_: # t的长度等于len
pos = 0
for i in range(1, tlen + 1):
pos = Next[pos][ord(t[i]) - ord('a')]
if pos == 0: break
ok = (pos == 0) # pos等于0说明无法匹配,此时为缺失子序列
print(1 if ok else 0)