−目次
日立製作所 社会システム事業部 プログラミングコンテスト2020 C,D,E問題メモ
Social Infrastructure Information Systems Division, Hitachi Programming Contest 2020
3完でそこそこのレートは出たけど、DかEのどちらかは解きたかったところ。
C - ThREE
問題
- N 頂点の木、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 sys def 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 grp def 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 ans n = 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 sys from bisect import bisect from itertools import accumulate n, 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 * * 10 t + = 1 t2 = t.bit_length() + 1 # dp[j]: (i番目まで見て) j件の店で買い物を完了した時の、次の店に到着できる最短時間 dp = [INF] * t2 dp[ 0 ] = 1 for _, 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 ] = tmp const_acc = list (accumulate(constant)) ans = 0 for 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_ans if 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演算で作り上げると楽だし高速。