AtCoder精選良問「D - Handstand」Diff 1138
この記事はAtCoderの精選良問集マガジンの中の記事です。
私が良問だと思った問題を解説付きでまとめています。
問題リンク
問題概要
N人の人が一列に並んでいる。それぞれ0か1で表されて、0の時は直立していて、1の時は逆立ちをしている。
区間を指定して、1と0を反転させる操作をK回まで行える。
適切に操作を行ったときに、逆立ちしている人(つまり1)を最大で何人並べられるか出力する。
考え方
まずは入力例2を確認してみる。
14 2
11101010110011
2回までの操作で1を何個最大で連続させられるか、というのがこの問題で考えるべきこと。
まず、考察の一つとして、「0の区間だけひっくり返すのが最適」というのがある。
これは考えると当然のことで、例えば1001みたいな場合、間のゼロを2つひっくり返すと、1111になって4つ繋げられる。1を含む形でひっくり返すと、必ず0の区間をひっくり返すよりもパフォーマンスが悪くなる。
つまり、「0の区間をK個までひっくり返して、1を最大で何個続けられるか?」という問題に言い換えることができる。
こういう2種類の文字のみが続く問題は、ランレングスエンコードをして考えるのが、鉄則。
ランレングスエンコードとはaabbという文字があった時に、[[a,2],[b,2]]みたいに圧縮する手法。
入力例を圧縮すると、以下のようになる。
// 11101010110011
[[1,3], [0,1], [1,1], [0,1], [1,1], [0,1], [1,2], [0,2], [1,2]]
操作をすると、0の区間は最大でK個まで1にできる。
つまり、0の区間を最大でK個まで保持して、1をどれくらい続けられるか、を考えればいい。
これには尺取法のアルゴリズムを用いる。
尺取法の解説はいろいろなところにあるので、詳しい説明は省くが、「右端をできるだけ伸ばす。条件を満たさなくなったら、左端を縮める」みたいな感じ。この場合の条件は0の区間がK個以内、ということ。
入力例でいうと、右端を伸ばしていくと、最初の5つまでで0区間がK個、つまり2つになる。
[1,3], [0,1], [1,1], [0,1], [1,1]
この時の長さは、3 + 1 + 1 + 1 + 1 = 7
そこに次の[0,1]という配列がやってくる。
長さを足すと 7 + 1 で8になるが、このとき0を3つ含んでいるので、この長さは答えにすることはできない。
そこで、0がK個に収まるように左端を取り除いていく。
0の区間数を2個以内にするためには、
[1,3], [0,1]の2つの区間を捨てる必要がある。
そのため、長さとしては 8 - 4 で4になる。
このように最大でK個の0の区間を保持して、右端を伸ばしながら左端を捨てていく。過程で計算された長さの最大長を答えればACとなる。
実装
import { createInterface } from "readline";
import * as fs from "fs";
import { log } from "console";
import { exit } from "process";
/**
*
* @param s 文字列
* @returns aabb => [['a',2],['b',2]]
*/
export const runLengthEncode = (s: string): Array<[string, number]> => {
if (s.length == 0) return []
const encoded: Array<[string, number]> = [];
let currentChar = s[0];
let count = 1;
for (let i = 1; i < s.length; i++) {
if (s[i] === currentChar) {
count++;
} else {
encoded.push([currentChar, count]);
currentChar = s[i];
count = 1;
}
}
encoded.push([currentChar, count]);
return encoded;
}
function main() {
const [n, k] = nextNums(2)
const s = next()
// ランレングスエンコードする
const rle = runLengthEncode(s)
// 答え
let ans = 0
// ゼロ区間の個数
let zeroCount = 0
// 左端
let left = 0
// 1を続けられる長さ
let length = 0
for (let right = 0; right < rle.length; right++) {
const [char, cnt] = rle[right]
// 0区間だったら足す
if (char == '0') zeroCount++
// 長さを足す
length += cnt
// ゼロ区間がk個以内になるまで左端を捨てる
while (zeroCount > k) {
// 捨てる区間
const [removeChar, removeCnt] = rle[left]
// 長さを縮める
length -= removeCnt
// 0を捨てたらカウントダウン
if (removeChar == '0') zeroCount--
// 左端をインクリメント
left++
}
ans = Math.max(ans, length)
}
log(ans)
}
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();
}
この記事が気に入ったらサポートをしてみませんか?