AtCoder精選良問「B - Values」Diff 732
この記事はAtCoderの精選良問集マガジンの中の記事です。
私が良問だと思った問題を解説付きでまとめています。
問題リンク
問題概要
N頂点M辺の単純無向グラフで、各辺は二つの頂点を結んでいる。
はじめ、頂点 i には値 ai が書かれている。
そのような設定で以下の操作を行う。
操作:一つの辺を選び、その辺が結ぶ頂点の一つの値を1増やし、もう一つの値を1減らす、またはその逆の操作を行う。
操作後に各頂点の値をそれぞれ b1,b2,⋯⋯,bNにすることができるか、出力する問題。
考え方
まず、配列を2つ考えるのは見通しが悪いので、差分を取って操作しなければいけない数字の配列を作る。
1 2 3
2 2 2
入力例1の場合はこんな数列が与えられるので、B-Aで差分を取ると、以下のようになる。
1 0 -1
とりあえず、この差分を適切に操作して全て0にすることを聞いている問題なんだな、ということを理解する。
次に操作に目を向ける。
操作は二つの頂点を選んで片方を-1,片方を+1するような操作だった。
重要な考察として、操作をした数列の総和自体は変化しない。
ということは、数列の総和が0でない時点で、絶対に全て0にすることはできない。
逆に考えると「総和が0だったら絶対にできるんじゃないか」と考える。
例えばこんな数列があったとする。
5 -7 2 -3 3
総和は0。
左から貪欲に操作すると絶対に0にできる。
// 1回目
0 -2 2 -3 3
// 2回目
0 0 0 -3 3
// 3回目
0 0 0 0 0
グラフ自体は単純グラフであるが、いくつかのグループに分かれることがあるようだ。入力例3のグラフをプロットするとこんな感じ。
ということは各グループごとの差分配列の総和が0になるか考えればよい。
これはUnionFindなどのデータ構造を使うと比較的簡単に実装できる。
実装
import { createInterface } from "readline";
import * as fs from "fs";
import { log } from "console";
import { exit } from "process";
export class UnionFind {
private parents: number[];
constructor(private n: number) {
this.parents = Array.from({ length: n }, () => -1);
}
public find(x: number): number {
if (this.parents[x] < 0) {
return x;
} else {
this.parents[x] = this.find(this.parents[x]);
return this.parents[x];
}
}
public union(x: number, y: number): void {
x = this.find(x);
y = this.find(y);
if (x === y) {
return;
}
if (this.parents[x] > this.parents[y]) {
[x, y] = [y, x];
}
this.parents[x] += this.parents[y];
this.parents[y] = x;
}
public size(x: number): number {
return -this.parents[this.find(x)];
}
public same(x: number, y: number): boolean {
return this.find(x) === this.find(y);
}
public members(x: number): number[] {
const root = this.find(x);
return Array.from({ length: this.n }, (_, i) => i).filter(i => this.find(i) === root);
}
public roots(): number[] {
return Array.from({ length: this.n }, (_, i) => i).filter(i => this.parents[i] < 0);
}
public groupCount(): number {
return this.roots().length;
}
public allGroupMembers(): Map<number, number[]> {
const groupMembers = new Map<number, number[]>();
for (let member = 0; member < this.n; member++) {
const root = this.find(member);
if (!groupMembers.has(root)) {
groupMembers.set(root, []);
}
groupMembers.get(root)!.push(member);
}
return groupMembers;
}
}
function main() {
const [n, m] = nextNums(2)
const arrA = nextNums(n)
const arrB = nextNums(n)
// 差を取る
const diff: number[] = []
for (let i = 0; i < n; i++) {
diff.push(arrB[i] - arrA[i])
}
// UnionFindを作成
const uf = new UnionFind(n)
// 辺ごとに頂点をグルーピングする
for (let i = 0; i < m; i++) {
let [a, b] = nextNums(2)
// 0インデックスに直す
a--
b--
// unionする
uf.union(a, b)
}
let ans = 'Yes'
// 各メンバーを繰り返す
for (const elm of uf.allGroupMembers()) {
const members = elm[1]
let arrSum = 0
for (const vertex of members) {
arrSum += diff[vertex]
}
// 0じゃなかったらできない
if (arrSum != 0) {
ans = 'No'
}
}
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();
}
この記事が気に入ったらサポートをしてみませんか?