「アルゴリズム コンペティション: クイック 300 問」は 2024 年に出版される予定で、「アルゴリズム コンペティション」の補助問題集です。
すべての質問は、自作の OJ New Online Judgeに配置されます。
コードは C/C++、Java、Python の 3 つの言語で提供されており、トピックは主に中レベルから低レベルのトピックであり、初級レベルから上級レベルの学生に適しています。
「 プレフィックス圧縮テクノロジー 」、リンク: http://oj.ecustacm.cn/problem.php?id=2140
質問の説明
[問題の説明] Web サイトには現在 n 人のユーザーがいます。各ユーザー名は長さ L の文字列です。すべてのユーザー名を保存するには、n*L 文字を保存する必要があります。
メモリを圧縮する方法が追加されました。各ユーザーの完全なユーザー名を保存する代わりに、他のユーザー名に同じプレフィックスがない限り、そのユーザー名のプレフィックスのみを保存します。
たとえば、名前が james と jacob だけの場合、jam と jac のみが保存され、両方とも認識されます。
この圧縮技術を使用すると、n 個のユーザー名を保存するには何文字が必要になりますか?
[入力形式] 1行目は正の整数n、L、2≦n≦10000、1≦L≦1000です。入力は n * L ≤ 1000000 であることが保証されます。
次の n 行には、各行に小文字のみを含む長さ L の文字列が含まれており、すべての文字列は互いに異なります。
[出力形式]答えを表す整数を出力します。
【入力サンプル】
样例1:
2 5
james
jacob
样例2:
4 4
xxxx
yxxx
xxyx
yxxy
【出力サンプル】
样例1:
6
样例2:
14
答え
シミュレーションと辞書ツリーの 2 つの方法を使用して解決します。
【ポイント】辞書ツリー。
C++ コード-1
一つ目の方法はシミュレーションです。この質問では、異なるプレフィックスがいくつあるかを数える必要があります。手順は次のとおりです:
(1) すべての文字列を並べ替えます。
(2) 2 つの隣接する文字列のプレフィックスを継続的に比較します。異なる文字が見つかったら停止し、プレフィックスの長さを一度記録します。次に、次の 2 つの隣接する文字列の比較を続けます。
#include<bits/stdc++.h>
using namespace std;
string s[10001];
int ans[10001];
int main(){
int n, L; cin >> n >> L;
for(int i = 0; i < n; i++) cin >> s[i];
sort(s, s + n); //字符串排序
int sum = 0;
for(int i = 0; i < n - 1; i++){
//连续比较字符串s[i] 和 s[i+1]
for(int j = 0; j < L; j++){
ans[i] = max(ans[i], j+1);
ans[i+1] = max(ans[i+1], j+1);
if(s[i][j] != s[i+1][j]) break; //第j个字符不同
}
sum += ans[i]; //统计第i个字符串的前缀:它的前j个字符和其他字符都不同
}
cout<<sum + ans[n-1];
return 0;
}
C++ コード-2
以下では、辞書ツリー エンコーディングが使用されています (辞書ツリー テンプレート、清華大学出版局、Luo Yongjun、Guo Weibin、「アルゴリズム コンペティション」、562 ページを参照)。
まず辞書ツリーtree[]を構築します。Tree[u].son[v] は、u 番号のノードの次の文字が v であるノードの番号を表します。
次に、Insert() を使用して、すべての文字列を辞書ツリーに挿入します。辞書ツリーのルートノードからこの文字列の文字が出現するかどうかを調べ、出現する場合、このプレフィックスの番号は num++ です。num=1 の場合、このプレフィックスは 1 回だけ出現し、一意であることを意味します。
最後に、Find() を使用して、すべての異なるプレフィックスの合計長をカウントします。つまり、num=1 でプレフィックスをクエリし、その長さを累積します。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
struct node{
int son[26]; //26个字母
int num; //这个前缀出现的次数
}t[N]; //字典树tree[u].son[v]:编号为u的节点的下一个字母为v的节点的编号
int cnt = 1; //当前分配的存储位置。把cnt=0留给根节点
void Insert(char *s) {
//往字典树中插入字符串s
int now = 0; //从根节点开始找插入前缀的位置
for(int i = 0; s[i]; i++) {
int ch = s[i]-'a';
if(t[now].son[ch]==0) //这个字符还没有存储过
t[now].son[ch] = cnt++; //把cnt位置分配给这个字符
now = t[now].son[ch]; //沿着字典树往下走
t[now].num++; //统计这个前缀出现多少次
}
}
int Find(int u, int len){
if(t[u].num == 1) return len; //这个前缀只出现了1次,说明是唯一的前缀
int ans = 0;
for(int i = 0; i < 26; i++) //遍历整个字典树,统计只出现一次的前缀
if(t[u].son[i])
ans += Find(t[u].son[i], len+1);
return ans;
}
char s[1010];
int main(){
int n, L; scanf("%d%d", &n, &L);
while(n--){
scanf("%s", s);
Insert(s);
}
cout<<Find(0, 0)<<endl; //从根节点0开始找,此时字符长度为0
return 0;
}
Java コード-1
シミュレーション
import java.util.Arrays;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int L = scanner.nextInt();
String[] s = new String[n];
for (int i = 0; i < n; i++) s[i] = scanner.next();
Arrays.sort(s); // 字符串排序
int[] ans = new int[n];
int sum = 0;
for (int i = 0; i < n - 1; i++) {
// 连续比较字符串s[i] 和 s[i+1]
for (int j = 0; j < L; j++) {
ans[i] = Math.max(ans[i], j+1);
ans[i+1] = Math.max(ans[i+1],j+1);
if (s[i].charAt(j) != s[i + 1].charAt(j)) break; // 第j个字符不同
}
sum += ans[i]; // 统计第i个字符串的前缀:它的前j个字符和其他字符都不同
}
System.out.println(sum + ans[n - 1]);
}
}
Java コード-2
辞書ツリー
import java.util.*;
public class Main {
static class Node {
int[] son = new int[26];
int num;
}
static Node[] t = new Node[1000010];
static int cnt = 1;
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int L = input.nextInt();
for (int i = 0; i < t.length; i++)
t[i] = new Node();
for (int i = 0; i < n; i++) {
String s = input.next();
Insert(s);
}
System.out.println(Find(0, 0));
}
static void Insert(String s) {
int now = 0;
for (int i = 0; i < s.length(); i++) {
int ch = s.charAt(i) - 'a';
if (t[now].son[ch] == 0) {
t[now].son[ch] = cnt++;
}
now = t[now].son[ch];
t[now].num++;
}
}
static int Find(int u, int len) {
if (t[u].num == 1) return len;
int ans = 0;
for (int i = 0; i < 26; i++)
if (t[u].son[i] != 0)
ans += Find(t[u].son[i], len + 1);
return ans;
}
}
Python コード-1
シミュレーション
n, L = map(int, input().split())
s = []
for i in range(n): s.append(input())
s.sort() # 字符串排序
ans = [0] * n
sum = 0
for i in range(n - 1): # 连续比较字符串s[i] 和 s[i+1]
for j in range(L):
ans[i] = max(ans[i], j + 1)
ans[i+1] = max(ans[i + 1], j + 1)
if s[i][j] != s[i+1][j]: break # 第j个字符不同
sum += ans[i] # 统计第i个字符串的前缀:它的前j个字符和其他字符都不同
print(sum + ans[n - 1])
Python コード-2
辞書ツリー
import sys
sys.setrecursionlimit(1000000)
class Node:
def __init__(self):
self.son = [0] * 26
self.num = 0
t = [Node() for _ in range(1000010)]
cnt = 1
def Insert(s):
global cnt
now = 0
for c in s:
ch = ord(c) - ord('a')
if t[now].son[ch] == 0:
t[now].son[ch] = cnt
cnt += 1
now = t[now].son[ch]
t[now].num += 1
def Find(u, length):
if t[u].num == 1: return length
ans = 0
for i in range(26):
if t[u].son[i] != 0:
ans += Find(t[u].son[i], length + 1)
return ans
n, L = map(int, input().split())
for _ in range(n):
s = input().strip()
Insert(s)
print(Find(0, 0))