《算法竞赛·快冲300题》每日一题:“最短缺失子序列”

算法竞赛·快冲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)

猜你喜欢

转载自blog.csdn.net/weixin_43914593/article/details/132522551