AtCoder精選良問「D - Unique Username」Diff 1309
この記事はAtCoderの精選良問集マガジンの中の記事です。
私が良問だと思った問題を解説付きでまとめています。
問題リンク
問題概要
文字列探索系の問題。
まず入力は以下のようなルール。
N個の文字列S1,S2,…SNが与えられる
M個の文字列T1,T2,…TMが与えられる
そして以下の条件に合致するXを出力する
N 個の文字列 S1,S2,S3...SNを好きな順番で並び変えたものを、S'1,S'2,S'3...S'Nとする。
XはS'1{一つ以上の"_"}{S'2}{一つ以上の"_"}{S'3}{一つ以上の"_"}...{S'N}というルールで作る。
Xは3文字以上16文字以内
XはT1,T2…TMに含まれない
考え方
Nの制約が8と小さくXの最大長も16と少ないので、なんとなく全探索ができそうだなーということをまず考える。
まずNの順番を好きに並び替える、という部分で計算量を見積もる。
順列並び替えの生成なので8!=40,320通り。愚直に探索したとしても、間に合いそう。
順列についてはPythonなどではitertools.permutationを使えば簡単に生成できる。
TypeScriptでは以下のような関数を用意すれば同じようなことができる。
/**
*
* @param items
* @returns 順列をyieldして返す
*/
export function* permutations<T>(items: T[], prefix: T[] = []): Generator<T[]> {
if (items.length === 0) {
yield prefix;
} else {
for (let i = 0; i < items.length; i++) {
const newPrefix = prefix.concat(items[i]);
const remainingItems = items.slice(0, i).concat(items.slice(i + 1));
yield* permutations(remainingItems, newPrefix);
};
};
};
問題となるのはそれぞれの文字の間に{一つ以上の"_"}をくっつける部分。
例えば入力が以下のような場合
choku
dai
それぞれの文字数は5と3なので、16文字以内でありうる"_"の数は1~8となる。入力が2文字しかなければこれでいいが、たとえば3文字あったらどうだろう。
choku
dai
san
合計の文字数は11文字なので、残りで打てる"_"の数は5文字。二か所打つ必要があるので、組み合わせを考えると、[1,1],[1,2],[1,3],[1,4],[2,1],[2,2],[2,3],[3,1],[3,2],[4,1]というようになる。
文字の並び替えは比較的簡単にできそうなので、入力に応じて、打てる"_"の組み合わせを列挙することがこの問題の肝となる。
たとえば配列の長さが決まっていれば全列挙することはできる。こういう感じになるだろう。
function main() {
const numbers: number[][] = []
// たとえば長さが3で足して5以下になる組み合わせ
const limit = 5
const length = 3
for (let a = 1; a <= limit - (length - 1); a++) {
for (let b = 1; b <= limit - a - 1; b++) {
for (let c = 1; c <= limit - a - b; c++) {
numbers.push([a, b, c])
}
}
}
log(numbers)
}
[
[ 1, 1, 1 ], [ 1, 1, 2 ],
[ 1, 1, 3 ], [ 1, 2, 1 ],
[ 1, 2, 2 ], [ 1, 3, 1 ],
[ 2, 1, 1 ], [ 2, 1, 2 ],
[ 2, 2, 1 ], [ 3, 1, 1 ]
]
Nの制約が8と小さいのでひたすらパターンを書いてもなんとかいけそうな感じはある。しかし、こういうケースでは再帰的な関数を書くのが一般的なようだ。
const dfs = (remain: number, depth: number, length: number): number[][] => {
// 残りがなかったら何も返さない
if (remain <= 0) return []
// 最後まで達したら残りのありうる数字を返す
if (depth == length) {
const ret = []
for (let i = 1; i <= remain; i++) {
ret.push([i])
}
return ret
}
const ret = []
for (let i = 1; i <= remain; i++) {
// 再帰的に生成する
for (const j of dfs(remain - i, depth + 1, length)) {
ret.push([i, ...j])
}
}
return ret
}
dfs関数では残りの数字をあらわすremainと今みている配列の長さをあらわすdepthと返す配列の長さのlengthを受け取る。
再帰には終了条件を記述する必要がある。
まず残りが0以下だったら作れる数字がないので、空の配列を返すようにする。
// 残りがなかったら何も返さない
if (remain <= 0) return []
次にdepthが配列の長さに達した時は、ありうる数字を全て返す。例えば合計が5以下の配列で、最後に達した時に3残っていたとすると、[1],[2],[3]を返す必要がある。
// 最後まで達したら残りのありうる数字を返す
if (depth == length) {
const ret = []
for (let i = 1; i <= remain; i++) {
ret.push([i])
}
return ret
}
あとは再帰的に数字を繰り返せばいい。
const ret = []
for (let i = 1; i <= remain; i++) {
// 再帰的に生成する
for (const j of dfs(remain - i, depth + 1, length)) {
ret.push([i, ...j])
}
}
この関数を使う時は以下のように合計値、深さ1、配列の長さを指定する。
log(dfs(5,1,3))
例えばdepth=1,i=1のときには、次にdfs(remain=4,depth=2,length=3)が呼び出される。
そうすると、その中で二つめのiが1から4まで繰り返される。
二つ目のiが1の時はdfs(remain=3,depth=3,length=3)が呼び出される。
そうするとdepth==lengthの終了条件がtrueになり、[1],[2],[3]が返される。
return されたので、depth=2のi=1にこの返り値が含まれてでdepth=1にreturnされる
[1,…[1]]=>[1,1]
[1,…[2]]=>[1,2]
[1,…[3]]=>[1,3]
そしてdepth=1に戻ってくる。
[1,…[1,1]]=>[1,1,1]
[1,…[1,2]]=>[1,1,2]
[1,…[1,3]]=>[1,1,3]
これを再帰的に繰り返していくことで、合計N以下の長さLの数字の組み合わせを列挙することができる。
実装
あとはこの関数を使って問題を解いていく。
コーナーケースとしてN=1の場合に注意をする。
import { createInterface } from "readline";
import * as fs from "fs";
import { log } from "console";
/**
*
* @param items
* @returns 順列をyieldして返す
*/
export function* permutations<T>(items: T[], prefix: T[] = []): Generator<T[]> {
if (items.length === 0) {
yield prefix;
} else {
for (let i = 0; i < items.length; i++) {
const newPrefix = prefix.concat(items[i]);
const remainingItems = items.slice(0, i).concat(items.slice(i + 1));
yield* permutations(remainingItems, newPrefix);
};
};
};
const dfs = (remain: number, depth: number, length: number): number[][] => {
// 残りがなかったら何も返さない
if (remain <= 0) return []
// 最後まで達したら残りのありうる数字を返す
if (depth == length) {
const ret = []
for (let i = 1; i <= remain; i++) {
ret.push([i])
}
return ret
}
const ret = []
for (let i = 1; i <= remain; i++) {
// 再帰的に生成する
for (const j of dfs(remain - i, depth + 1, length)) {
ret.push([i, ...j])
}
}
return ret
}
function main() {
const [n, m] = nextNums(2)
// S1,S2,...SNを記録
const arr: string[] = []
// 文字数の合計数を記録
let lengthSum = 0
for (let i = 0; i < n; i++) {
const char = next()
arr.push(char)
lengthSum += char.length
}
// 使われている文字を集合にする
const taken: Set<string> = new Set()
for (let i = 0; i < m; i++) {
taken.add(next())
}
// コーナーケース
if (n == 1) {
const leng = arr[0].length
if (!taken.has(arr[0]) && 3 <= leng && leng <= 16) {
log(arr[0])
} else {
log('-1')
}
process.exit()
}
// 残りの打てる"_"の数
const remainLength = 16 - lengthSum
// 組み合わせをdfsで全列挙。それぞれの文字の間に"_"を打つので、長さはn-1
const numbers: number[][] = dfs(remainLength, 1, n - 1)
// 順列を繰り返す
for (const chars of permutations(arr)) {
// 打てる"_"を全通り探索
for (const nums of numbers) {
// 答え候補
let tmp = ''
// 答えを生成
for (let i = 0; i < n - 1; i++) {
tmp += chars[i]
tmp += '_'.repeat(nums[i])
}
// n-1の繰り返しなので、最後の文字をくっつける
tmp += chars[n - 1]
// 使われていない
if (!taken.has(tmp)) {
// 3文字以上16文字以下なら出力して終了
if (3 <= tmp.length && tmp.length <= 16) {
log(tmp)
process.exit()
}
}
}
}
// ここまで来たら当てはまるものがない
log('-1')
}
let inputs = "";
let inputArray: string[];
let currentIndex = 0;
function next() {
return inputArray[currentIndex++];
}
function nextNum() {
return +next();
}
function nextBigInt() {
return BigInt(next());
}
function nexts(length: number) {
const arr = [];
for (let i = 0; i < length; ++i) arr[i] = next();
return arr;
}
function nextNums(length: number) {
const arr = [];
for (let i = 0; i < length; ++i) arr[i] = nextNum();
return arr;
}
function nextBigInts(length: number) {
const arr = [];
for (let i = 0; i < length; ++i) arr[i] = nextBigInt();
return arr;
}
// デバッグ環境がWindowsであれば条件分岐する
if (process.env.OS == "Windows_NT") {
const stream = createInterface({
input: process.stdin,
output: process.stdout,
});
stream.on("line", (line) => {
inputs += line;
inputs += "\n";
});
stream.on("close", () => {
inputArray = inputs.split(/\s/);
main();
});
} else {
inputs = fs.readFileSync("/dev/stdin", "utf8");
inputArray = inputs.split(/\s/);
main();
}
この記事が気に入ったらサポートをしてみませんか?