ベスパリブ

プログラミングを主とした日記・備忘録です。ベスパ持ってないです。

ABC 136: D問題の解説 (Python3)

問題:AtCoder Beginner Contest 136: D - Gathering Children

公式解説PDF:https://img.atcoder.jp/abc136/editorial.pdf

公式解説動画:https://youtu.be/lyHk98daDJo?t=3172

解説

発想としてはランレングス法です。

S=RRRL という例を考えます。


  • i=0の人は最終的にどこにいるのか?
## 初期状態
i: 0 1 2 3
-----------
S: R R R L
   .
## 3回の操作のあと
i: 0 1 2 3
-----------
S: R R R L
         .
## 4回の操作のあと
i: 0 1 2 3
-----------
S: R R R L
       .

という風に、あとは奇数回の操作後はi=3のLに、偶数回の操作後はi=2のRにいることになります。


  • i=1の人は最終的にどこにいるのか?
## 初期状態
i: 0 1 2 3
-----------
S: R R R L
     .
## 2回の操作のあと
i: 0 1 2 3
-----------
S: R R R L
         .
## 3回の操作のあと
i: 0 1 2 3
-----------
S: R R R L
       .

という風に、あとは偶数回の操作の後はi=3のLに、奇数回の操作の後はi=2のRにいることになります。


以上の考察から、初期位置がRの場合で、初期位置から折返し地点までの距離を d とすると(ここで折り返し地点とは、操作中に到達するLのこと)、

  • dが奇数のとき、
    • 偶数回の操作の後は折返し地点の1つ左側に収束
    • 奇数回の操作の後は折返し地点に収束
  • dが偶数のとき、
    • 偶数回の操作の後は折返し地点に収束
    • 奇数回の操作の後は折返し地点の1つ左側に収束

ということがわかります。


同様に、初期位置がLの場合で、初期位置から折返し地点までの距離を d とすると(ここで折り返し地点とは、操作中に到達するRのこと)、

  • dが奇数のとき、
    • 偶数回の操作の後は折返し地点の1つ右側に収束
    • 奇数回の操作の後は折返し地点に収束
  • dが偶数のとき、
    • 偶数回の操作の後は折返し地点に収束
    • 奇数回の操作の後は折返し地点の1つ右側に収束

ということがわかります。


そして操作は10100回の偶数回行われるので、偶数回の操作後のことのみ考えれば良いことがわかります。

以上のような考察を紙にお絵かきしてシミュレーションすると、これはO(N)で実装できそうです。

ここまで気づいたら、あとはどうバグらせずシミュレーションを実装できるかという話なのですが、

文字列はRグループとLグループに交互に分けられそうだと思いつきます。

たとえば RRRLRLRLLLRRL という文字列を考えた場合、

 R R R  L  R  L  R  L L L  R R  L
|_____||_||_||_||_||_____||___||_|
   3    1  1  1  1    3     2   1

というふうに各グループの人数を数えることで、シミュレーションを簡単に実装できそうです。この部分がランレングス法の発想です。


まずはRグループについて考えます。

  • グループの人数を cnt
  • 折返し地点であるLまでの距離が偶数の人数を even_num
  • 折返し地点であるLまでの距離が奇数の人数を odd_num

とそれぞれすると、

cnt = 3
even_num = cnt//2
odd_num = cnt - even_num

で求められるので、

  • even_num 人は最終的に折返し地点に収束する
  • odd_num 人は最終的に折返し地点の1つ左側に収束する

ことがわかります。


次にLグループについて考えます。

Rグループのときと同様の考察でcnt, even_num, odd_num を求めて、

  • even_num 人は最終的に折返し地点に収束する
  • odd_num 人は最終的に折返し地点の1つ右側に収束する

ことがわかります。

コーナーケース

  • 特になし

実装

def solve():
    S = input()
    N = len(S)

    ans = [0]*N  # 最終的に出力する答え(10**100操作後に、各iに人がいる人数)

    # Rグループについて考える
    cnt = 0  # 現在のRグループの人数
    for i, moji in enumerate(S):
        if moji == "R":
            cnt += 1
            continue
        else:
            even_num = cnt//2         # 折返し地点までの距離が偶数の人の数
            odd_num = cnt - even_num  # 折返し地点までの距離が奇数の人の数
            ans[i] += even_num        # 折返し地点までの距離が偶数の人は、ans[i]に収束する
            ans[i-1] += odd_num       # 折返し地点までの距離が奇数の人は、ans[i-1]に収束する
            cnt = 0

    # Lグループについて考える
    cnt = 0  # Lグループの人数
    for i in range(N-1, -1, -1):
        if S[i] == "L":
            cnt += 1
            continue
        else:
            even_num = cnt//2
            odd_num = cnt - even_num
            ans[i] += even_num
            ans[i+1] += odd_num
            cnt = 0

    print(*ans)


if __name__ == "__main__":
    solve()

計算量は O(N) です。

ABC 031: C問題の解説 (Python3)

問題:AtCoder Beginner Contest 031: C - 数列ゲーム

公式解説PDF:https://www.slideshare.net/chokudai/abc031

公式解説動画:なし

解説1:力技

Nは高々50なので、  O(N^3) くらいまでいけそうだとあたりをつけます。

全探索で、問題をシミュレートして高橋のとりうる得点の最大値を計算します。

コーナーケース

  • 特になし

実装

import sys


def solve():
    N = int(input())
    As = list(map(int, sys.stdin.readline().split()))

    ans = -float("inf")  # 出力する答え

    # i := 高橋が選んだ数列の番号
    # j := 青木が選んだ数列の番号
    for i in range(N):
        aoki = []  # 青木がとりうる可能性のある得点をすべて格納する
        for j in range(N):
            if i == j: continue

            left, right = min(i,j), max(i,j)
            Ts = As[left: right+1]  # 数列T
            aoki.append(sum(Ts[1::2]))

        # 青木が最大の得点を得るときの 青木が選んだjをもとに、高橋の得点を計算する     
        j = aoki.index(max(aoki))
        left, right = min(i, j), max(i, j)
        Ts = As[left: right+1]
        ans = max(ans, sum(Ts[0::2]))

    print(ans)


if __name__ == "__main__":
    solve()

aoki.append(sum(Ts[1::2])) の部分の sum(Ts[1::2]) と、 ans = max(ans, sum(Ts[0::2])) の部分の sum(Ts[0::2]) の計算量が  O(N) 回かかりそうなので、全体の 計算量は  O(N^3 ) ですかね?あんまり自信ないです。

解説2:累積和

与えられた数列As の偶数番号の累積和と、奇数番号の累積和をあらかじめ計算しておくことで、青木の得点を O(1) で計算できるようになり、全体の計算量を  O(N^2) にできます。

入力例2で考えます。

数列をAsとして、偶数番号の累積和と、奇数番号の累積和を合体した数列dpをあらかじめ計算します。

 i:  0  1  2  3  4  5
---------------------
As:  1 -3  3  9  1  6
dp:  1 -3  4  6  5 12

dp[4]には、As[0], As[2], As[4] の累積和が格納されています。

dp[5]には、As[1], As[3], As[5] の累積和が格納されています。

  • たとえば高橋が1番(left), 青木が3番(right)を選んだときの数列Tsは、
 i:  0  1  2  3  4  5
---------------------
As:  1 -3  3  9  1  6
dp:  1 -3  4  6  5 12
Ts:    -3  3  9

となり、このときの青木の得点は3ですが、これはあらかじめ計算したdpを使えば、

青木の得点:dp[right-1] - dp[left-1] = 3

高橋の得点:dp[right] = 6

になります。

  • たとえば高橋が5番(right), 青木が2番(left)を選んだときの数列Tsは、
 i:  0  1  2  3  4  5
---------------------
As:  1 -3  3  9  1  6
dp:  1 -3  4  6  5 12
Ts:        3  9  1  6

となり、このときの青木の得点は15ですが、これはあらかじめ計算したdpを使って、

青木の得点:dp[right] - dp[left-1] = 15

高橋の得点:dp[right-1] - dp[left-2] = 4

になります。

このように、累積和を使って高橋と青木の得点計算を高速でおこなうことができるようになります。

あとは全探索で、青木の得点が最大値のときの、高橋の得点を計算していき、高橋の得点の最大値を算出します。

コーナーケース

  • 特になし

実装

import sys


def solve():
    N = int(input())
    As = list(map(int, sys.stdin.readline().split()))

    # 累積和(dp?)
    dp = [0]*N
    dp[0] = As[0]
    dp[1] = As[1]
    for i in range(2, N):
        dp[i] = dp[i-2] + As[i]

    # 全探索
    # i := 高橋が選んだ番号
    # j := 青木が選んだ番号
    ans = -float("inf")  # 出力する答え
    for i in range(0, N):
        aoki_max = -float("inf")  # 青木の最大の得点
        aoki_max_j = None         # 青木の最大の得点のときの番号
        for j in range(0, N):
            # 青木が最も多く得点を得られるときの j(aoki_max_j) を決める
            if i == j: continue
            left, right = min(i,j), max(i,j)

            if (right-left)%2 == 0:
                if left-1 >= 0:
                    aoki = dp[right-1] - dp[left-1]
                else:
                    aoki = dp[right-1]
            else:
                if left-1 >= 0:
                    aoki = dp[right] - dp[left-1]
                else:
                    aoki = dp[right]
            
            if aoki > aoki_max:
                aoki_max = max(aoki_max, aoki)
                aoki_max_j = j
        
        # 青木が aoki_max_j を選んだときの高橋の点数を計算する
        left, right = min(i, aoki_max_j), max(i, aoki_max_j)
        
        if (right-left)%2 == 0:
            if left-2 >= 0:
                takahashi = dp[right] - dp[left-2]
            else:
                takahashi = dp[right]
        else:
            if left-2 >= 0:
                takahashi = dp[right-1] - dp[left-2]
            else:
                takahashi = dp[right-1]

        # 高橋が得られる得点として考えられる最大値を更新する
        ans = max(ans, takahashi)

    print(ans)
            

if __name__ == "__main__":
    solve()

編集履歴

[2019/09/12] 解説1の計算量が間違っていたっぽいので修正。解説2の方法の、コーナーケースが不要だったので削除。それに伴いコードも微修正。

ABC 140: D問題の解説 (Python3)

問題:AtCoder Beginner Contest 140: D - Face Produces Unhappiness

公式解説PDF:https://img.atcoder.jp/abc140/editorial.pdf

公式解説動画:https://youtu.be/VSeggcnxwrc?t=3564

ABC140のD問題のPython3による解説です。

初見では全くわからず、解説動画の解説と言っても過言ではないのだ。

以下の解説は公式解説動画でやっていることほぼまんまなので、まずはそちらを視聴することをおすすめします。

解説

考察問題です。ランレングス法みたいな考え方をします。

与えられた文字列Sについて、Lが連続している部分列を「Lグループ」、Rが連続している部分列を「Rグループ」とし、LグループとRグループの塊に分けて考えます。すると、

  • LグループとRグループの境目を選択して操作をすると、基本的に幸福な人の数を+2できる
  • ただし、端っこの境目を選んだときのみ、+1になる
  • 一度の操作で幸福な人を増やせる数は最大で+2でしかない

ということに気づけばOKです。

以下は、入力例2について考えてみます。

## 初期状態
 L R R L R L R R L R L L R
   .         .         .     幸福な人:3## LグループとRグループの境目を選択して、操作をする

 L R R L R L R R L R L L R
  |___|

 L L L L R L R R L R L L R
   . . .     .         .     幸福な人:5## LグループとRグループの境目を選択して、操作をする

 L L L L R L R R L R L L R
        |_|

 L L L L L L R R L R L L R
   . . . . . .         .     幸福な人:7## LグループとRグループの境目を選択して、操作をする

 L L L L L L R R L R L L R
            |___|

 L L L L L L L L L R L L R
   . . . . . . . .     .     幸福な人:9## LグループとRグループの境目を選択して、操作をする

 L L L L L L L L L R L L R
                  |_|

 L L L L L L L L L L L L R
   . . . . . . . . . . .     幸福な人:11## LグループとRグループの境目を選択して、操作をする

 L L L L L L L L L L L L R
                        |_|

 L L L L L L L L L L L L L
   . . . . . . . . . . . .   幸福な人:12

以上の考察からすると、初期状態の幸福な人の数を score とすると、

  • K回操作して、さらに 2K 人を幸福にすることができたパターン = score + 2K
  • K回以下の操作で、すべての人を L(R) にすることができたパターン = N-1

のどちらかなので、最終的な幸福な人の数 ans は、

ans = min(score+2*K, N-1) 

となります。

コーナーケース

  • 特になし

実装

def solve():
    N, K = list(map(int, input().split()))
    S = input()

    ans = 0
    for i in range(N-1):
        if S[i] == S[i+1]:
            ans += 1

    ans = min(ans + 2*K, N-1)
    print(ans)


if __name__ == "__main__":
    solve()

初期状態の幸福な人の数 score をO(N)で数えればよいだけなので、計算量は  O(N)

TODO

解説PDFを読むと左端にRを、右端にLを置いて考える考察があります。解説PDFをシミュレートする実装をしたのですがコーナーケースっぽい testcase_19 でWAになったので、テストケースが公開されたら再考しようと思います。

pywintypes.com_error: (-2147352561, 'パラメーターはオプションではありません。', None, None) エラーの意味

  File "test/test_main.py", line 225, in hoge
    wp = wps.AddByPoint()
  File "C:\Users\XXXX\AppData\Local\Temp\gen_py\3.7\D98A091D-3A0F-4C3E-B36E-61F62068D488x0x1x0.py", line 125299, in AddByPoint
    , Construction)
pywintypes.com_error: (-2147352561, 'パラメーターはオプションではありません。', None, None)

「関数やメソッドの引数が足りないよ」という意味。ここでいうパラメーターとは引数のこと。「そのメソッドの引数はオプション引数じゃないよ。必須の引数だよ」という意味。

上の例だと、 AddByPoint() の引数が足りないです。

参考

win32comとかPyWin32とかPython for Windows extensionsとかいろいろな名前で呼ばれているが同じもののようだ。

win32comはPythonからimoprtするモジュール名。

途方も無いWin32のエラーコード集

ABC 129: D問題の解説 (Python3)

問題:AtCoder Beginner Contest 129: D - Lamp

公式解説PDF:https://img.atcoder.jp/abc129/editorial.pdf

公式解説動画:https://youtu.be/L8grWxBlIZ4?t=4363

ABC129のD問題のPython3による解説です。

解説

DP問題です。

これは公式の解説動画がとてもわかりやすかったので、そちらを見るのをおすすめします。

問題を解くアイデアとしては、i行j列目に明かりを設置したとき、

  • 左方向に照らせるマスの数を格納した L
  • 右方向に照らせるマスの数を格納した R
  • 上方向に照らせるマスの数を格納した U
  • 下方向に照らせるマスの数を格納した D

のそれぞれを作成します。L, R, U, DはシンプルなDPによって計算することができます。

すると、i行j列目に明かりを設置したとき、上下左右に明かりを照らせるマスの数を格納したものを ans_grid とすると、

ans_grid[i][j] = L[i][j] + R[i][j] + U[i][j] + D[i][j] - 3

で導出することができるので、あとは ans_grid の要素の最大値を出力すればOKです。

コーナーケース

  • 特になし

実装

# -*- coding:utf-8 -*-
import sys
import numpy as np

def solve():
    H, W = list(map(int, sys.stdin.readline().split()))
    grid = []
    for _ in range(H):
        grid.append(list(input()))
    grid = (np.array(grid) == ".")*1  # ランプが置ける:1, 障害物:0

    #print(grid)

    L = np.zeros((H,W), dtype=int)  # 左方向に照らせるマスの数
    R = np.zeros((H,W), dtype=int)  # 右方向に照らせるマスの数
    U = np.zeros((H,W), dtype=int)  # 上方向に照らせるマスの数
    D = np.zeros((H,W), dtype=int)  # 下方向に照らせるマスの数

    # 左方向に照らせるマスの数のDPを埋める
    for i in range(W):
        if i == 0:
            L[:, i] = grid[:, i]
        else:
            L[:, i] = (L[:, i-1] + 1) * grid[:, i]

    # 右方向に照らせるマスの数のDPを埋める
    for i in range(W-1, -1, -1):
        if i == W-1:
            R[:, i] = grid[:, i]
        else:
            R[:, i] = (R[:, i+1] + 1) * grid[:, i]

    # 上方向に照らせるマスの数のDPを埋める
    for i in range(H):
        if i == 0:
            U[i] = grid[i]
        else:
            U[i] = (U[i-1] + 1) * grid[i]

    # 下の方向に照らせるマスの数のDPを埋める
    for i in range(H-1, -1, -1):
        if i == H-1:
            D[i] = grid[i]
        else:
            D[i] = (D[i+1] + 1) * grid[i]

    # 上下左右すべてのDPを埋めた。
    # よって、i行j列目に明かりを設置したとき、上下左右に照らせるマスの数を格納した ans_grid は、
    ans_grid = L + R + U + D - 3
    ans = np.max(ans_grid)
    print(ans)


if __name__ == "__main__":
    solve()

計算量は、 O(HW)

単純なforループでやろうとするとTLEしたので、行列計算を高速でしてくれるnumpyを使っています。

numpyの重要な特徴

narrayに対する演算

L = np.zeros((4,3), dtype=int)  # 全ての要素が0の、4行3列の行列を作成
print(L+1)
# [[1 1 1]
#  [1 1 1]
#  [1 1 1]
#  [1 1 1]]

行を順番に取り出す

R = np.arange(4*3).reshape(4, 3)  # 4行3列の行列を作成
print(R)
# [[ 0  1  2]
#  [ 3  4  5]
#  [ 6  7  8]
#  [ 9 10 11]]


# 行を順番に取り出す
for i in range(4):
    print(R[i])
# [0 1 2]
# [3 4 5]
# [6 7 8]
# [ 9 10 11]

列を順番に取り出す

for i in range(3):
    print(R[:, i])
# [0 3 6 9]
# [ 1  4  7 10]
# [ 2  5  8 11]

ABC 130: D問題の解説 (Python3)

問題:AtCoder Beginner Contest 130: D - Enough Array

公式解説PDF:https://img.atcoder.jp/abc130/editorial.pdf

公式解説動画:https://youtu.be/ERZuLAxZffQ?t=2473

解説

ABC130のD問題の解説です。しゃくとり法の問題です。

8 10
6 1 2 7 10 1 2 1

という入力を考えます。 この入力は、

  • N = 8
  • K = 10
  • 数列は 6 1 2 7 10 1 2 1

です。

## 初期状態
 6 1 2 7 10 1 2 1
|_|
 6

left = 0  # 部分列の左端の添字(0-origin)
right = 0  # 部分列の右端の添字(0-origin)
ans = 0  # 出力する答え

## rightを右に1進める
 6 1 2 7 10 1 2 1
|___|
  7

## rightを右に1進める
 6 1 2 7 10 1 2 1
|_____|
   9

## rightを右に1進める
 6 1 2 7 10 1 2 1
|_______|
    16

部分列の和がKを超えた。
重要なポイントとして、この時点からrightを右に1移動させた部分列 6 1 2 7 10 の和もKを超える。
さらにrightを右に1移動させた部分列 6 1 2 7 10 1 の和もKを超える。
つまり、この時点から right を一番右に移動させるまでの N-right個 の部分列は必ずKを超えるので、まとめて足してやればよい。
ans += N - right

## total < K になるまで、leftを右に1進める
 6 1 2 7 10 1 2 1
  |_____|
    10

同様に、N-right個をまとめて足してやる。
ans += N - right

## total < K になるまで、leftを右に1進める
 6 1 2 7 10 1 2 1
    |___|
      9

## rightを右に1進める
 6 1 2 7 10 1 2 1
    |______|
       19

ans += N - right

## total < K になるまで、leftを右に1進める
 6 1 2 7 10 1 2 1
      |____|
        17

ans += N - right

## total < K になるまで、leftを右に1進める
 6 1 2 7 10 1 2 1
        |__|
         10

ans += N - right

## total < K になるまで、leftを右に1進める
 6 1 2 7 10 1 2 1
           |
           0

## rightを右に1進める
 6 1 2 7 10 1 2 1
           |_|
            1

## rightを右に1進める
 6 1 2 7 10 1 2 1
           |___|
             3

## rightを右に1進める
 6 1 2 7 10 1 2 1
           |_____|
              4

rightはこれ以上右に進めないので終了

コーナーケース

  • 特になし

実装

# -*- coding:utf-8 -*-
import sys


def solve():
    N, K = list(map(int, input().split()))
    As = list(map(int, sys.stdin.readline().split()))

    left = 0  # 部分列の左端の添字
    total = 0  # 部分列の和
    ans = 0  # 出力する答え
    for right in range(0, N):
        total += As[right]
        while total >= K:
            ans += N - right
            total -= As[left]
            left += 1

    print(ans)


if __name__ == "__main__":
    solve()

計算量は、 O(N)

公式解説によると、二分探索による解法もある。

ABC140: C問題の解説 (Python3)

問題:AtCoder Beginner Contest 140: C - Maximal Value

公式解説PDF:https://img.atcoder.jp/abc140/editorial.pdf

公式解説動画:https://youtu.be/VSeggcnxwrc?t=2461

解説

ABC140のC問題の解説です。貪欲法(?)問題です。

数列  A_i を リストAsに、数列  B_i を リストBsに置き換えて考えます。AsとBsは0-originです。

Bs[i] >= max(As[i], As[i+1]) の条件を満たすようにAs[i+1] の値を貪欲的に埋めていく問題です。

最初に初期値のAs[0]とAs[1]を決めます。これはBs[0]とBs[1]の大きさを比較すれば一意に定まります。

たとえば、Bs[0]=10, Bs[1]=20のとき、Bs[i] >= max(As[i], As[i+1]) より、As[0] =10, As[1] = 10 と求まります。

    # 初期値を決める
    As = [0] * N
    if Bs[0] <= Bs[1]:
        # As:  ?   ?   →   As: 10  10
        # Bs: 10  20   →   Bs: 10  20
        As[0] = As[1] = Bs[0]
    else:
        # As:  ?   ?   →   As: 20  10
        # Bs: 20  10   →   Bs: 20  10
        As[0], As[1] = Bs[0], Bs[1]

あとは Bs[i] >= max(As[i], As[i+1]) の条件を満たすようにAs[i+1] の値を貪欲的に埋めていけばOKです。

数列の最後のAs[N-1] だけ例外で、これは必ずBs[N-2]になります。

    # 貪欲法
    for i in range(1, N-2):
        if Bs[i] > Bs[i+1]:
            # As:   ■    ?  →  As:   ■    0
            # Bs: 153    0  →  Bs: 153    0
            As[i+1] = Bs[i+1]
        else:
            # As:   ■    ?  →  As:   ■  153
            # Bs: 153  160  →  Bs: 153  160
            As[i+1] = Bs[i]
    As[N-1] = Bs[N-2]

コーナーケース

  • N == 2 の場合、答えは必ず Bs[0]*2 になる

実装

# -*- coding:utf-8 -*-
import sys

"""
Bs[i] >= max(As[i], As[i+1]) を満たす条件で、

sum(As)の最大値を求める問題
"""

def solve():
    N = int(sys.stdin.readline())
    Bs = list(map(int, sys.stdin.readline().split()))

    if N == 2:
        print(Bs[0]*2)
        return

    # 初期値を決める
    As = [0] * N
    if Bs[0] <= Bs[1]:
        # As:  ?   ?   →   As: 10  10
        # Bs: 10  20   →   Bs: 10  20
        As[0] = As[1] = Bs[0]
    else:
        # As:  ?   ?   →   As: 20  10
        # Bs: 20  10   →   Bs: 20  10
        As[0], As[1] = Bs[0], Bs[1]

    # 貪欲法
    for i in range(1, N-2):
        if Bs[i] > Bs[i+1]:
            # As:   ■    ?  →  As:   ■    0
            # Bs: 153    0  →  Bs: 153    0
            As[i+1] = Bs[i+1]
        else:
            # As:   ■    ?  →  As:   ■  153
            # Bs: 153  160  →  Bs: 153  160
            As[i+1] = Bs[i]
    As[N-1] = Bs[N-2]

    print(sum(As))


if __name__ == "__main__":
    solve()

計算量は、 O(N)

リスト As を作っていく方式だが、最大値のみを求めればよいので、別にリストAsはつくらなくても良い。その場合は計算時間と使用メモリを少し小さくできる。