−目次
全国統一プログラミング王決定戦本戦 A~E問題メモ
A - Abundant Resources
問題
- N 要素の数列 A1,A2,...,AN
- 1~N のそれぞれについて(K とする)以下を求めよ
- 「連続する K 個の要素の和」の最大値
解法
まず A について累積和をとる。これを Acc とする。
連続する K 要素の和は、Acc から、Acc を K 要素ずらした配列を引くと求まる。
A 10 20 30 40 50
Acc 0 10 30 60 100 150
K=3 0 10 30 60 100 150
- 0 10 30
= 60 90 120 → 最大値は120
1 2 3 4 5 6 7 8 |
import numpy as npn = int(input())aaa = np.fromiter(map(int, input().split()), dtype=np.int64)aaa = np.insert(aaa, 0, 0)aaa = np.cumsum(aaa)buf = [(aaa - np.roll(aaa, k))[k:].max() for k in range(1, n + 1)]print('\n'.join(map(str, buf))) |
B - Big Integers
問題
- 長さ N の整数列 A、長さ M の整数列 B、整数 K が与えられる
- 値 X,Y を以下のように定義する
- X=N∑i=1Ai×KN−i=A1×KN−1+A2×KN−2+...+AN×K0
- Y=M∑i=1Bi×KM−i=B1×KM−1+B2×KM−2+...+BM×K0
- X と Y のどちらが小さいか求めよ
- 0≤Ai,Bi≤K−1
- 1≤A1,B1
解法
ややこしい書き方をしているが、K 進法同士の比較。
桁数が小さい方、同じなら上位桁から見ていって、最初に小さい数字が現れた方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def solve(n, m, k, aaa, bbb): if n < m: return 'X' if n > m: return 'Y' if aaa < bbb: return 'X' if aaa > bbb: return 'Y' return 'Same'n, m, k = map(int, input().split())aaa = list(map(int, input().split()))bbb = list(map(int, input().split()))print(solve(n, m, k, aaa, bbb)) |
C - Come Together
問題
- H×W の各マス目にコマが1つずつ置かれている
- K 個のコマを取り除く((R1,C1),(R2,C2),...,(RK,CK) で与えられる)
- 取り除いた後、以下の操作を繰り返して、盤面に残る全てのコマを1箇所のマスに集める
- 1つのコマを、上下左右に隣接するいずれかのマスに移動させる
- 操作回数の最小値を求めよ
解法
どこのマスに集めるかを決める。
縦に動かす動作・横に動かす動作は互いに干渉しないので、縦横を独立に考えられる。なので以下 X 座標を考える。
仮に集める座標を x とする。 これを x+1 に変更すると、x より右にある全てのコマの必要回数が1回ずつ減り、x 以左にある全てのコマの必要回数が1回ずつ増える。
なので、最適な xa は、全てのコマの X 座標の中央値となる。
または、下に凸の形をしているので、三分探索でも求められる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import bisectimport sysimport numpy as npdef search(aaa, size): l, r = 0, size coef_base = np.arange(size, dtype=np.int64) while l < r: m1 = (l + r) // 2 m2 = m1 + 1 cost1 = (abs(coef_base - m1) * aaa).sum() cost2 = (abs(coef_base - m2) * aaa).sum() if cost1 < cost2: r = m1 elif cost1 > cost2: l = m2 else: return cost1 return (abs(coef_base - l) * aaa).sum()h, w, k = map(int, input().split())rrr = np.full(h, w, dtype=np.int64)ccc = np.full(w, h, dtype=np.int64)for line in sys.stdin: r, c = map(int, line.split()) rrr[r - 1] -= 1 ccc[c - 1] -= 1print(search(rrr, h) + search(ccc, w)) |
D - Deforestation
問題
- 竹が N 本あり、初期の高さはいずれも0。時刻1につき1m伸びる
- M 回、竹を切る
- i 回目に切る時は、時刻 Ti に、[Li,Ri] の範囲の竹を切る
- 切った竹の長さの合計値が得点として加算される
- 切られた竹は高さ0になるが、再び伸び続ける
- 最終的に得られる得点を求めよ
解法
途中で何回切ったかに関わらず、ある竹1本から得られるスコアは、その竹を「最後に切った時刻」に等しくなる。
なので、RangeUpdateQuery を処理できるセグメント木を作成し、時刻で更新していき、最後に1つ1つの最終的な数を合計すればよい。
セグ木はそれなりに配列参照が多く、Pythonでは間に合わないこともあるためPyPyを検討することになるが、PyPyは関数の再帰呼び出しがあまり得意では無いため、なるべく再帰を使わない書き方で実装した方がよいっぽい。
セグ木上の各要素にsentinel以外の値が入っていれば、その区間の下は全て同じ値、ということにしている。(変数名が実態と合ってないのでよくない)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
import sysclass RangeUpdateTree: def __init__(self, n, initial=0, sentinel=-1): n2 = 1 << n.bit_length() self.n = n2 self.offset = n2 self.data = [initial] * (n2 << 1) self.sentinel = sentinel def update(self, a, b, x): stack = [(1, 0, self.n)] while stack: i, l, r = stack.pop() if a <= l and r <= b: self.data[i] = x continue y = self.data[i] j = i << 1 if y != self.sentinel: self.data[i] = self.sentinel self.data[j] = self.data[j + 1] = y m = (l + r) // 2 if a < m: stack.append((j, l, m)) if m < b: stack.append((j + 1, m, r)) def aggregate(self): stack = [1] ans = 0 while stack: i = stack.pop() if self.data[i] != self.sentinel: ans += self.data[i] * (self.n >> i.bit_length() - 1) continue i <<= 1 stack.append(i) stack.append(i + 1) return ans def debug_print(self): i = 1 while i <= self.offset: print(self.data[i:2 * i]) i <<= 1n, m = map(int, input().split())rut = RangeUpdateTree(n)for line in sys.stdin: t, l, r = map(int, line.split()) rut.update(l - 1, r, t)print(rut.aggregate()) |
E - Erasure
問題
- 箱が N 個、横一列に並んでいる
- 整数 K が与えられる
- 連続する箱の選び方のうち、長さ K+1 以上の区間は、(N−K)(N−K+1)/2 通りある
- つまり、それぞれの区間を選ぶか選ばないかで「区間の選び方の組」は 2(N−K)(N−K+1)/2 通りある
- このうち、全ての箱がいずれかの区間に入っているような「区間の選び方の組」の個数を mod109+7 で求めよ
- 1≤N≤5000
解法
以下の説明では、以下の記号を用いる。
1 2 ... i |------------------| 1~iを覆う単一の区間 |==================| 1~iを覆える区間の組(単一とは限らないが、いずれかでは覆われている) |..................| 特に覆う覆わないに関係なく、1~iの範囲
ひとまず、こんなDPを試してみる。
DP[j]=1~j 個目の箱まで覆うが、j+1 個目の箱はまだ覆われていないような区間の組の選び方
これで DP[j] から DP[j+1] に遷移を考える。j+1 を覆わねばならず、j+2 は覆われてはいけないので、j+1 が右端である区間をどれか1つは使わなくてはならない。
1 2 3 .. j-K .. j j+1
|======================| DP[j]
|---------------------------| ┐
|------------------------| │ j-K+1 個の内
|---------------------| │ 少なくともどれか1つあればいい
... │ DP[j+1] = DP[j] * (2^(j-K+1) - 1) ???
|-------------| ┘
という感じで遷移したいが、以下のようなケースで困る。
|---------------------| ←例えばこれを追加するときは
|========| |------| 追加区間で覆われていない区間さえ覆われていれば、
途中で途切れていてもよい
この途中で途切れた選び方の数は、DPに入っていない
改めてDPをこう定義する。
DP[i][j]=右端が i 個目までで終わる区間だけを使って、1~j 個目の箱は覆われ、j+1 個目は覆われていないような区間の組の選び方
DP[N][N] が答えとなる。実際は、i+1 を求める際には i の情報があれば良いので、DP[j] を逐次的に更新していくことで1次元配列で実装できる。
違いは、途切れた区間でもパターン数を数え上げられる点。
1 j i
|========| |------| |--| 表現するパターン数
^ ^
旧DP[j] 新DP[i][j]
1~j まで覆われ、j+1 が覆われていなければ、「j+2~i はどうなっていてもよい」という部分を織り込んだ値となる。
DP[i][i]の遷移
DP[i−1][1~i−1] まで求められていて、DP[i][1~i] を求める。まずは、DP[i][i] を求める。
i の箱は覆わなければならないが、i+1 の箱は覆われてはいけないため、「i を右端に持つ区間」を1つは使う必要がある。
1~iを覆う単一の区間を使う場合
1 2 3 ... i
|-----------------|
|----| ←残りは自由
この場合、他の区間は完全に使うも使わないも自由。他の区間は (i−K−1)×(i−K+2)/2 個あるので、選び方は 2(i−K−1)×(i−K+2)/2 通りある。
1~iを覆う区間を使わない場合
場合分けのため、i を右端に持つ区間の内、最も長いものを x~i とする。
すると、遷移元が DP[i−1][x−1]~DP[i−1][i−1] のいずれかであれば、1~i が全て覆われた状態を保てる。 また、自身より短いi を右端に持つ区間 x+1~i,x+2~i,... は加えても加えなくても良い。
よって、i−1∑z=x−1(DP[i−1][z])×2i−x−K 通りが加えられる。
1 2 3 .. x-1 x ... i
|-----------------|
遷移元
|==============| DP[i-1][x-1]
|==================| DP[i-1][x]
...
|=============================| DP[i-1][i-1]
新規任意追加
|-------------|
|----------| i-x-K 個
|------|
x は2~i−K まで動くので、i−K∑x=2i−1∑z=x−1(DP[i−1][z])×2i−x−K となる。うわあ、式で書くとややこしい。
でもこれ、DP[i-1]の後ろからの累積和と、2の累乗を事前計算しておけば、それらのドット積となり、そこそこ簡単にはなる。
K=2
1 2 3 4 5 6 7
DP[i-1] 0 0 1 5 50 904
ACC 960 960 960 959 954 904
2^x 8 4 2 1 __ __ (末尾K個は不使用)
----------------------------
ACC*2^x 7680 3840 1920 959 --合計-> 14399
これに区間 1~i を使う場合の214=16384 を加えて、30783 が DP[i][i] となる。
DP[i][j]の更新
DP[i][j] は、DP[j][j](旧DP[j])に、j+2~i−1 の間の箱だけで取れる区間のパターン数をかけた値となる。
1 j j+2 i-1 i
|========| |...............|
^ ここだけで何パターンできるか?
旧DP[j]
なので区間の長さ i−j が K+1 に満たないと1(何も置かない)
置ける場合は、DP[i-1][j] の値に「2i−1が右端となる区間の数」をかけていくと更新できる。
K=2
1 2 3 4 5 6 7 8
DP[7] 0 0 2 5 50 904 30783
16 8 4 2 ※ ※ ※ ※: 置けない
-----------------------------------------
DP[8] 0 0 8 10 50 904 30783 2032629
これで DP[i] が全て求まったので、N まで繰り返せば答えとなる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import numpy as npn, k = map(int, input().split())MOD = 10 ** 9 + 7pow2 = [0] * (n - k + 1)pow2[0] = 1for i in range(1, n - k + 1): pow2[i] = pow2[i - 1] * 2 % MODpow2 = np.array(pow2, dtype=np.int64)dp = np.array([0] * k + [1, 5], dtype=np.int64)p = 5for i in range(k + 2, n): if i > (k + 1) * 2: tmp = dp[k:i - k - 1] * pow2[:i - 2 * k - 1][::-1] % MOD dp[k:i - k - 1] = tmp dp_acc = np.cumsum(dp[::-1]) % MOD dp = np.append(dp, (pow(2, p, MOD) + ((dp_acc[k:] * pow2[:i - k]) % MOD).sum()) % MOD) p += i - k + 2print(dp[n - 1]) |
解説pdfにあった、DP[i][j]=i より左の区間のみを使って、1~j が覆われて j+1 がまだ覆われていないような区間の組の選び方 とする解法(後で考えたら i の条件が右端→左端になっただけ?)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
n, k = map(int, input().split())MOD = 10 ** 9 + 7pow2 = [0] * (n - k + 1)pow2[0] = 1for i in range(1, n - k + 1): pow2[i] = pow2[i - 1] * 2 % MODdp = [0] * ndp_acc = [1] * nfor i in range(n - k): ndp = [0] * n for j in range(i + k, n): ndp[j] = (dp[j] * pow2[j - i - k + 1] + dp_acc[j - 1] * pow2[j - i - k]) % MOD dp = ndp if i > k: r = dp_acc[i - 1] for j in range(i - 1, n): dp_acc[j] -= r if i == 0: dp_acc[k - 1] = 0 for j in range(i + k, n): dp_acc[j] = (dp_acc[j - 1] + dp[j]) % MODprint(dp[-1]) |

