「アルゴリズムコンテスト・クイック300問」 1日1問:「最短欠損列」

アルゴリズム コンペティション: クイック 300 問」は 2024 年に出版される予定で、「アルゴリズム コンペティション」の補助問題集です。
すべての質問は、自作の OJ New Online Judgeに配置されます。
コードは C/C++、Java、Python の 3 つの言語で提供されており、トピックは主に中レベルから低レベルのトピックであり、初級レベルから上級レベルの学生に適しています。


最短の欠落サブシーケンス 」、リンク: 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 が与えられると、文字列 t が s の最も短い欠落部分列であるかどうかを m 回尋ねます。
[入力形式] 1 行目は与えられた小文字セット v で、長さは [1,26] で、各文字は 1 回だけ出現します。
それ以降、入力文字列内のすべての文字は v に属します。
   2 行目は文字列 s、1≤|s|≤1000000 です。
   3 行目は正の整数 m で、問い合わせの数を示します (1≤m≤1000000)。
   次の m 行の各行には、各クエリ文字列 1≤|t|≤1000000 を表す文字列 t が含まれています。
   入力により、すべてのクエリ文字列の長さの合計が 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 = "abbaccabac" を例として、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] に存在します。「abbac」には存在しない、「ca」などの長さ 2 の最も短い欠落部分列。
   2 回目のチェックで 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] で見つかった場合、2 番目の文字は s[i+1] ~ s[j] の 2 番目のラウンドで見つかります。文字の 2 番目のラウンドでは、最後の s[j] がこのラウンドで初めて出現することに注意してください。
   長さ 3 の最も短い欠落部分列については、次のように構築できます。最初のラウンドの最後の文字 s[i] と 2 番目のラウンドの最後の文字 s[j] にさらに 1 文字を加えます。これは、長さ 3 の最も短い欠落サブシーケンスです。欠落サブシーケンス。この構造の正しさは次のように簡単に説明されます。 s の文字の最初の 2 ラウンドが "***c***b" であるとします。ここで、"***c" は最初のラウンド、c は最後で唯一のラウンドです。 1 番目、「** *b」が 2 番目、「b」が最後で唯一の 1 番目であるため、「***c***b」に「cb*」が現れないことは簡単に証明できます。は最も短い欠落サブシーケンスです。たとえば、s[1] ~ s[9] = "abbac-ccab" であることを確認すると、長さ 3 の最も短い欠落部分列には、"cba"、"cbb"、"cbc" などが含まれます。ただし、このようにして構築された最短欠損配列にはそれらすべてが含まれるわけではなく、例えば「caa」も最短欠損配列ではありますが、構築された3つの配列の中には含まれません。
   複数ラウンドのチェックの後、ラウンド +1 に等しい len が取得されます。
   エンコード時に、文字の各ラウンドにすべての v 文字が含まれているかどうかを判断するにはどうすればよいですか? これは単純にバイナリで処理されます。vK を定義します。バイナリ内の各 '1' は、v に存在する文字を表します。たとえば、v = "abc"、vK =...000111、'a' は最後の '1'、'b' は最後の '1' に対応します。 '1'、'c' は 3 番目の '1' に対応します。同様に、各ラウンドの s に存在する文字は、sK によってバイナリで表現されます。vK = sK の場合、このラウンドの s の文字には v のすべての文字が含まれます。
   len を見つけるプロセスは貪欲です。
  
   質問 (2): 長さが len の文字列 t は、欠落している s の部分列の中で最も短いものですか?
   まず、t の文字が s にあるかどうかを 1 つずつ調べる総当たり法を検討します。最初の文字 t[1] は、s[i] で初めて t[1] が見つかったと仮定します。2 番目の文字t[2 ]、s[j];... で見つかったと仮定して、s[i+1] から検索を続けます。t のすべての文字が s にあるかどうかがチェックされるまで続きます。a t のクエリを作成する計算量は O(n) で、m 回クエリを作成すると合計の計算量は O(mn) となり、タイムアウトになります。
   s[i] の後に各文字が最初に出現する位置を事前に計算しておくと、迅速に検索できます。Next[i][j] を定義して、s[i] の後に j 番目の文字が最初に現れる位置を表します。たとえば、s = "abbaccabac" の場合、添え字は 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 は、最初のタイプの文字 'b' が初めて出現する位置で、s[2] = 'b にあります。 '; Next[ 5][1] = Next[5]['b'-'a'] = 9 は、s[5] の後に最初に文字 'b' が出現することを表し、以下同様となります。
   Next[][] を使用すると、s 内の t を激しく検索する方が高速になります。まず最初の文字 t[1] をチェックします (つまり、pos = Next[0][t[1]-'a'] をチェックします); 次に 2 番目の文字 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