−目次
日立製作所 社会システム事業部 プログラミングコンテスト2020 C,D,E問題メモ
Social Infrastructure Information Systems Division, Hitachi Programming Contest 2020
3完でそこそこのレートは出たけど、DかEのどちらかは解きたかったところ。
C - ThREE
問題
- NN 頂点の木、i 番目の辺は ai と bi を結ぶ
- 以下の条件を満たすように、各頂点に 1~N の数字を1つずつ割り当てる
- 頂点 i に割り当てられた数字を pi とする
- 距離が3だけ離れた全ての頂点組 (i,j) について、pi,pj の和または積が3の倍数である
- 可能な割り当てがあるか判定し、可能なら割り当て方を1例求めよ
- 1≤N≤2×105
解法
pi が3の倍数であれば、i から3個離れた頂点の数字は何でもよい。 そうでなくて、たとえば 3k+1(3で割って1余る数)なら、3個離れた頂点の数字は「3の倍数」または「3k+2(3で割って2余る数)」でなければならない。
自身から3個離れた頂点が多い頂点から優先的に3の倍数を割り振るのが効率的?とか考えても、 そもそも3個離れた頂点のペアは最大 1010 近くになるのでとても列挙できないし、 3の倍数が尽きたときの処理が結局よく分からない。
3の倍数が関わらないペアでは「3k+1 と 3k+2 をペアにする」さえ守ればよくて、他の条件はよく考えると無いので、3の倍数と言いつつ注目すべきグループは2つしかない。
2部グラフを思いつく。
適当に値を決めて、根からの距離が偶数と奇数グループに分けると、距離が3だけ離れた頂点は必ず2つのグループから1つずつ選ばれる。
よって、一旦グループ分けすればグラフの具体的な形は無視してよくて、 一方を 3k+1、他方を 3k+2 から割り振っていって、足りないのを 3k で補うと条件を満たす。
(例) N = 10 偶数 1 4 7 10 3 (3k+1 と 3) 奇数 2 5 8 6 9 (3k+2 と 6 9)
ただし、この方針は注意があって、頂点数が一方のグループにやたら偏っていると上手くいかない。
どこまで偏っているとダメかというと、少ない方のグループが 3k+2 の個数より少なければ、もう一方にはみ出してしまう。
3k+1 1 4 7 10 3k+2 2 5 8 ←少ない方のグループの頂点数が3未満だと、 3k 3 6 9 上記の方法では矛盾が生じる可能性がある 偶数 1 4 7 10 3 6 9 ⑧ ← 3k+2 を割り振らざるを得ない 奇数 2 5
しかしその場合、少ない方のグループは 3k の個数以下である。(3k+1,3k+2,3k の個数の差は高々1)
少ない方のグループ全てに3の倍数を割り当てることが可能であり、するともう一方は何でもよくなるので、偏りがある場合はそうすればよい。
結局、どんな場合でも条件を満たすように構築できる。
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 56 57 |
import sysdef part(s, links): grp = [set(), set()] q = [(s, -1, 0)] while q: v, p, g = q.pop() grp[g].add(v) for u in links[v]: if u == p: continue q.append((u, v, g ^ 1)) return grpdef solve(n, links): grp = part(0, links) can = [[], [], []] for i in range(1, n + 1): can[i % 3].append(i) if len(grp[0]) > len(grp[1]): grp[0], grp[1] = grp[1], grp[0] ans = [0] * n if len(grp[0]) <= len(can[0]): for v in grp[0]: ans[v] = can[0].pop() can = can[0] + can[1] + can[2] for v in grp[1]: ans[v] = can.pop() return ans for v in grp[0]: if can[2]: ans[v] = can[2].pop() else: ans[v] = can[0].pop() for v in grp[1]: if can[1]: ans[v] = can[1].pop() else: ans[v] = can[0].pop() return ansn = int(input())links = [set() for _ in range(n)]for line in sys.stdin: a, b = map(int, line.split()) a -= 1 b -= 1 links[a].add(b) links[b].add(a)print(*solve(n, links)) |
D - Manga Market
問題
- N 個の店がある
- 高橋君は時刻0に自宅にいて、いくつかの店を回る予定を立てている
- 自宅から各店、また各店間の移動には全て1分かかる
- 時刻 t に店 i に着くと、行列のため買い物には ait+bi 分かかる
- 時刻 T までに、最大いくつの店で買い物が出来るか
- T ちょうどに買えたのはセーフだが、並んでる途中はアウト
- 移動時間と買い物時間以外の時間は無視できる
- 1≤N≤2×105
- 0≤ai,bi≤109
- 0≤T≤109
解法
優先順位付きナップサック。
店を回る順番まで考慮するには制約が大きいので、回る店の集合が決まったら、効率の良い順番は貪欲に決まるのでは?と疑う。
「店 i と j に続けて行くのなら、常に先に i に行った方がいい」という関係性が成り立たないかチェックする。それまでの買い物と移動に t かかったとして、
- i→j
- i 終了+移動: t+ait+bi+1=(ai+1)t+bi+1
- j 終了+移動: (aj+1)((ai+1)t+bi+1)+bj+1=Si→j
- j→i
- j 終了+移動: (aj+1)t+bj+1
- i 終了+移動: (ai+1)((aj+1)t+bj+1)+bi+1=Sj→i
で、Si→j<Sj→i となるための条件を移項して整理すると、綺麗に t が打ち消し合って消えてくれる。添字が同じものを同じ側に持っていくと、
- aibi+1>ajbj+1
となり、この指標が大きい方を先に(訪れるのであれば)訪れた方が、訪問時刻にかかわらずよいことがわかる。
以下のDPを考える。
- dp[i][k]= 指標の大きい方から i 番目の店までを見て、k 店で買い物を済ませたときの、最短時刻(次の店への移動含む)
しかしこれ、i も k も最大 N となり、O(N2) は間に合いそうにない。
ここで、この問題独特の考察が必要となる。つまり、「ai≥1 なら、常に t は1つの店ごとに2倍以上になる」
ai=1,bi=0 のミニマムを想定しても、買い物終了時刻(次の店に並びはじめられる時刻)は1店舗毎に 1→3→7→15→... となるので、意外と多くの店は回れない。
T の上限が 109<230 より、最大でも30店舗程度となる。
これならDPが回る。
ai≥1 の店舗だけのDPで、各 k について最短時間が求まったら、 そこから残り時間で ai=0 の店に何軒行けるか k 毎にチェックする。bi の小さい店から訪れた方がよい。
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 |
import sysfrom bisect import bisectfrom itertools import accumulaten, t = map(int, input().split())constant = []variable = []for line in sys.stdin: a, b = map(int, line.split()) if a == 0: constant.append(b + 1) else: variable.append((a / (b + 1), a, b))constant.sort()variable.sort(reverse=True)INF = 10 ** 10t += 1t2 = t.bit_length() + 1# dp[j]: (i番目まで見て) j件の店で買い物を完了した時の、次の店に到着できる最短時間dp = [INF] * t2dp[0] = 1for _, a, b in variable: for j in range(t2 - 1, -1, -1): if dp[j] == INF: continue tmp = (a + 1) * dp[j] + b + 1 if tmp > t: continue if dp[j + 1] > tmp: dp[j + 1] = tmpconst_acc = list(accumulate(constant))ans = 0for i, c in enumerate(dp): if c == INF: break remaining = t - c k = bisect(const_acc, remaining) ans = max(ans, i + k)print(ans) |
E - Odd Sum Rectangles
問題
- 2N−1 行 2M−1 列の各マスに、0または1を書き込む
- 左上 (i1,j1)、右下 (i2,j2) の矩形領域に含まれる数字の和を S(i1,i2,j1,j2) とする
- (i1,i2,j1,j2) の組は 2N(2N−1)2×2M(2M−1)2 通りあるが、全ての組について S を求めるとき、S が奇数となる組が最大となるような書き込み方を1つ構築せよ
- 1≤N,M≤10
解法
まず書き込み方が決まっているとして、その上で S が奇数となる個数を問う問題を考えてみる。
こういうのは2次元累積和で求められる。今回、偶奇にしか興味が無いので、XORで累積和を取る。先頭には便宜的に0を置く。
↓方向 →方向
(0 0 0) (0 0 0 0)
1 0 0 1 0 0 (0) 1 1 1
0 1 1 1 1 1 (0) 1 0 1
1 1 0 0 0 1 (0) 0 0 1
すると、ある矩形領域に含まれる数の和は、以下で求められる。
A│ B
──┼────┐ D - B - C + A
│ │
│ │ 今回は偶奇のみなので、^をXORの演算子として
C│ D│ A ^ B ^ C ^ D
└────┘
これが奇数になるとは、ABCDが (0,0,0,1) か (1,1,1,0) のように、3つが同じで1つだけ違うような状態になってるということである。 つまり「A=B なら C!=D」「A!=B なら C=D」となっている組が多いと嬉しい。
これを「理想の組」と呼ぶことにする。
矩形の和の問題が、累積和を取ることで、その4つ角の数字の組の問題になった。
後は、理想の組となる数を最大化した累積和盤面を作れれば、逆算して元の盤面を出力すればよい。
で、どうやって作るかをさっぱり思いつけず。
以下解説PDF読了後。
行、列の与えられ方が独特で、累積和を取ったときに便宜的に加えた1行を合わせるとちょうど2のべき乗となるので、 そこから構築方法を推察できなくもないが……難しい。
累積和を取った後の盤面の i 行 j 列目を fi,j で表す。
縦列 j1,j2 を固定して考える。fi,j1=fi,j2 となる行の個数を r0、fi,j1≠fi,j2 の行の個数を r1 とする。
j1 j2 0 0 同じ →r0 1 0 異なる →r1 1 1 同じ →r0 0 1 異なる →r1 ...
縦列を固定した上での理想の組の個数は、r0,r1 それぞれから1行ずつピックアップして組にすれば良いので r0×r1 となる。
r0+r1=2N という制約があるので、これは r0=r1=2N−1 の場合が(そういう風に置けるのであれば)最適である。
j1,j2 を固定したら、その中で最大化するには r0,r1 を同数にすることとわかった。 なので、そのような j1,j2 の組を出来るだけ増やしたい。 究極的に言うと、全ての j1,j2 の組で r0,r1 を同数にできたら、文句なくそれが最大だと言える。
希望条件: 全ての (j1,j2) の組で、r0,r1 が同数となるように0,1を配置できるか?
これは、N=M の盤面であれば以下のようにすると実現可能である。
N=M=1 から初めて、数学帰納法的に N=M=k (k=2,3,...)を作っていく。
まず累積和を取ったときの先頭行と先頭列は全て0なので、N=M=1 の時、右下には1を入れるのがよい。これで同数を達成できている(そもそも1組ずつしかない)。
0 0 0 1
ここから N=M=2 を考えると、縦と横には同じものを連結、そして右下の部分には、0,1を反転したものを置けばよい。
0 0 0 0 0 1 0 1 0 0 1 1 0 1 1 0
あたかも [0001] を1つの'0'と見做し、再帰的に同じように配置していく感じ。フラクタルっぽい。
解説を見たらそれで上手くいくことは分かっても、どうやったら思いつくのか、道筋が見えづらい。
希望条件を満たす盤面を元にして、まず縦にだけ2倍した盤面を作るなら、 元の盤面で任意の列の組において r0,r1 が同数であることが保証されているので、縦に繰り返しても同数なのは変わらない。 また、先頭列は'0'固定なので、それを踏まえても繰り返すのは自然な発想とは言える。
それを横に拡張するとき、ここでも単に繰り返すだけでも、だいたいの列においては r0,r1 が同数となるが、 唯一、拡張前に存在しなかった「同じ列(に相当する箇所)が選ばれる」場合のみ、当然ながら2列は全く一緒になり、条件を満たさなくなってしまう。
a b c d 縦・横ともに同じものを繰り返した盤面 0 0 0 0 0 1 0 1 (a,c), (b,d) が条件(r0=r1)を満たさない 0 0 0 0 0 1 0 1
それを解消する手段が、下半分だけ反転させる、ということになる。
希望条件を満たすなら「a列は全て0である」「a列と任意の列との組で、r0=r1 となる」以上、a列以外に含まれる'0'と'1'の個数は同数となっているはず。 それを踏まえれば「c列にも'1'が半分含まれないといけない」が、反転を思いつく取っかかりとなる……か?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
n, m = map(int, input().split())ans = [0, 1]for i in range(1, min(n, m)): k = 1 << i x = (1 << k) - 1 slide = [a << k for a in ans] new_ans = [s | a for s, a in zip(slide, ans)] new_ans.extend(s | (a ^ x) for s, a in zip(slide, ans)) ans = new_ansif n > m: ans *= 1 << (n - m)elif n < m: for i in range(n, m): k = 1 << i ans = [(a << k) | a for a in ans]ans = [a0 ^ a1 for a0, a1 in zip(ans, ans[1:])]ans = [a ^ (a >> 1) for a in ans]print('\n'.join(map(('{:0' + str(2 ** m - 1) + 'b}').format, ans))) |
Pythonは 21024 程度ならbit演算で作り上げると楽だし高速。

