Loading [MathJax]/jax/output/CommonHTML/jax.js

日立製作所 社会システム事業部 プログラミングコンテスト2020 C,D,E問題メモ

Social Infrastructure Information Systems Division, Hitachi Programming Contest 2020

3完でそこそこのレートは出たけど、DかEのどちらかは解きたかったところ。

C - ThREE

問題

  • N 頂点の木、i 番目の辺は aibi を結ぶ
  • 以下の条件を満たすように、各頂点に 1N の数字を1つずつ割り当てる
    • 頂点 i に割り当てられた数字を pi とする
    • 距離が3だけ離れた全ての頂点組 (i,j) について、pi,pj の和または積が3の倍数である
  • 可能な割り当てがあるか判定し、可能なら割り当て方を1例求めよ
  • 1N2×105

解法

pi が3の倍数であれば、i から3個離れた頂点の数字は何でもよい。 そうでなくて、たとえば 3k+1(3で割って1余る数)なら、3個離れた頂点の数字は「3の倍数」または「3k+2(3で割って2余る数)」でなければならない。

自身から3個離れた頂点が多い頂点から優先的に3の倍数を割り振るのが効率的?とか考えても、 そもそも3個離れた頂点のペアは最大 1010 近くになるのでとても列挙できないし、 3の倍数が尽きたときの処理が結局よく分からない。

3の倍数が関わらないペアでは「3k+13k+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 ちょうどに買えたのはセーフだが、並んでる途中はアウト
    • 移動時間と買い物時間以外の時間は無視できる
  • 1N2×105
  • 0ai,bi109
  • 0T109

解法

優先順位付きナップサック。

店を回る順番まで考慮するには制約が大きいので、回る店の集合が決まったら、効率の良い順番は貪欲に決まるのでは?と疑う。

「店 ij に続けて行くのなら、常に先に i に行った方がいい」という関係性が成り立たないかチェックする。それまでの買い物と移動に t かかったとして、

  • ij
    • i 終了+移動: t+ait+bi+1=(ai+1)t+bi+1
    • j 終了+移動: (aj+1)((ai+1)t+bi+1)+bj+1=Sij
  • ji
    • j 終了+移動: (aj+1)t+bj+1
    • i 終了+移動: (ai+1)((aj+1)t+bj+1)+bi+1=Sji

で、Sij<Sji となるための条件を移項して整理すると、綺麗に t が打ち消し合って消えてくれる。添字が同じものを同じ側に持っていくと、

  • aibi+1>ajbj+1

となり、この指標が大きい方を先に(訪れるのであれば)訪れた方が、訪問時刻にかかわらずよいことがわかる。

以下のDPを考える。

  • dp[i][k]= 指標の大きい方から i 番目の店までを見て、k 店で買い物を済ませたときの、最短時刻(次の店への移動含む)

しかしこれ、ik も最大 N となり、O(N2) は間に合いそうにない。

ここで、この問題独特の考察が必要となる。つまり、「ai1 なら、常に t は1つの店ごとに2倍以上になる」

ai=1,bi=0 のミニマムを想定しても、買い物終了時刻(次の店に並びはじめられる時刻)は1店舗毎に 13715... となるので、意外と多くの店は回れない。

T の上限が 109<230 より、最大でも30店舗程度となる。

これならDPが回る。

ai1 の店舗だけの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

問題

  • 2N12M1 列の各マスに、0または1を書き込む
  • 左上 (i1,j1)、右下 (i2,j2) の矩形領域に含まれる数字の和を S(i1,i2,j1,j2) とする
  • (i1,i2,j1,j2) の組は 2N(2N1)2×2M(2M1)2 通りあるが、全ての組について S を求めるとき、S が奇数となる組が最大となるような書き込み方を1つ構築せよ
  • 1N,M10

解法

まず書き込み方が決まっているとして、その上で 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のべき乗となるので、 そこから構築方法を推察できなくもないが……難しい。

累積和を取った後の盤面の ij 列目を fi,j で表す。

縦列 j1,j2 を固定して考える。fi,j1=fi,j2 となる行の個数を r0fi,j1fi,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=2N1 の場合が(そういう風に置けるのであれば)最適である。

j1,j2 を固定したら、その中で最大化するには r0,r1 を同数にすることとわかった。 なので、そのような j1,j2 の組を出来るだけ増やしたい。 究極的に言うと、全ての j1,j2 の組で r0,r1 を同数にできたら、文句なくそれが最大だと言える。

希望条件: 全ての (j1,j2) の組で、r0,r1 が同数となるように0,1を配置できるか?

これは、N=M の盤面であれば以下のようにすると実現可能である。

N=M=1 から初めて、数学帰納法的に N=M=kk=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演算で作り上げると楽だし高速。

programming_algorithm/contest_history/atcoder/2020/0308_hitachi2020.txt · 最終更新: 2020/03/12 by ikatakos
CC Attribution 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0