1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AtCoder ABC252 D - Distinct Trioを三種類の方法で解く: (組み合わせ/累積和の利用/DP)

Last updated at Posted at 2022-05-25

この問題を3つのアプローチで解き、また、類似する問題の考察を考えます。

  • 解法1: 組み合わせだけで考える(包除原理)
  • 解法2: 小さい方から見て作れる組み合わせ数を数える(累積和)
  • 解法3: DP

定義: 以下、$cnt_x$を配列$a$内に$x$の個数とします。

解法1: 組み合わせだけで考える(包除原理)

まず、異なるi, j, kを選ぶ組み合わせ全体を考えます。この組み合わせは、

  • パターン1:同じ値が含まれないもの
  • パターン2:同じ値が2個ちょうどであるもの
  • パターン3:同じ値が3個であるもの
    全てが包含されたものとなります。(同じ値が1つであるもの、というのは存在しません)
    今回は選択する数が3つであるため、これらは排他的です。(同時に複数のパターンを含むものはありません)

$ b > a$の時、$aCb = 0$です。

  • パターン2:各$x$に対して、$cnt_{x} C2 \times (n - cnt_{x}) $です。ある数$x$に注目した時、$x$から2つ選ぶ組み合わせは$cnt_{x}C2 $で、その中で$x$以外の数を選ぶのは、$n - cnt_{x}$です。
  • パターン3:各$x$に対して、$cnt_{x}$個の中から3つを選びたいので、$cnt_{x}C3$です

求めたい数(パターン1) = 全ての組み合わせ - パターン2 - パターン3を答えればよいです。

実装(Python)
from collections import defaultdict
nC3 = lambda n: (n * (n-1) * (n-2)) // (3*2*1)
nC2 = lambda n: (n * (n-1)) // (2*1)
n = int(input())
dat = list(map(int, input().split()))
cnt = defaultdict(int)
for x in dat: cnt[x] += 1

ans = nC3(n)
for key in cnt: ans -= nC2(cnt[key]) * (n - cnt[key])
for key in cnt: ans -= nC3(cnt[key])
print(ans)

解法2: 小さい方から見て作れる組み合わせ数を数える(累積和)

求めたい数の定義を考えると、相違なる$i$, $j$, $k$ を選んだ際の値$a[i]$, $a[i]$, $a[k]$の集合に含まれる異なる3つの値$x$,$y$,$z$は$x<y<z$となる値であることは自明です。(つまり、$a[i] = x$とは限らないです)
ここで、真ん中の数$y$を固定したとします。 このとき、$x$は$y$よりも小さい数全て、$z$は$y$よりも大きい数全てが候補になるので、その積の組み合わせを作ることができます。すなわち、ある$y$を定めた時には、配列aに含まれるyよりも小さい数の個数 * 配列aに含まれるyよりも大きい数の個数 というような$x$,$y$,$z$の組が、$配列aに含まれるyの個数$存在することになります。

実装を考えます。配列$a$がソートされているとし、値$x$の個数を$cnt[x]$としておきます。ある地点から右にある要素の数を$cntR$, 左にある要素の数を$cntL$とします。最初、$cntR = n$とします。次のように処理します。

  • xの小さい方から次の処理をします
  • 今、$x$をyとしてその左右の候補数を考えたいので、$cntR$から$cnt[x]$を引きます。
  • 答えに、$cntL \times cntR \times cnt[x]$を足します。
  • この処理が終わった後は、$x$は次の数字の左側になるので、$cntL$に$cnt[x]$を足します
  • これを繰り返します

$cntR$, $cntL$は実質、(それぞれ右、左からの)累積和です。

実装(Python)
from collections import defaultdict
n = int(input())
dat = list(map(int, input().split()))
cnt = defaultdict(int)
for x in dat: cnt[x] += 1

cntL = 0 # いまのところより左にある数の総数
cntR = n # いまのところより右にある数の総数
sortedNum = list(sorted(cnt.keys())) # 配列aの(distinctな)昇順の要素
ans = 0
for x in sortedNum:
    cntR -= cnt[x]
    ans += cntL * cntR * cnt[x]
    cntL += cnt[x]
print(ans)

解法3: DP

この時、$dp[i][j]$を昇順の$i$個めの要素まで$j$個を選んだとします。この時、dpの最後の要素の$[3]$が答えです。

配列$a$の小さい方から$i$個めの要素$x$についてdp[i][j] = dp[i-1][j] + dp[i-1][j-1] * cnt[x](ただし、j > 0で、j=0なら$dp[i][0] = 1$)です。(常に、0個しか選んでいない状態は1つです)
加算のそれぞれの項について考えます。

  • dp[i-1][j]: $i$個めの要素を選ばなかった場合、dp[i-1][j] について、直前の数まで$j$個を選んだ状態は引き継ぎます。
  • dp[i-1][j-1] * cnt[x]: $i$個めの要素を選んだ場合はdp[i-1][j-1]の状態に$x$を追加して、$j$個選んだ状態とします。このような$x$の選び方は$cnt[x]$個あります。
実装(Python)
from collections import defaultdict
n = int(input())
dat = list(map(int, input().split()))
cnt = defaultdict(int)
for x in dat: cnt[x] += 1

sortedNum = list(sorted(cnt.keys())) # 配列aの(distinctな)昇順の要素
sortedNumCount = len(sortedNum)
# dp[i][j]: 昇順のi個めの要素までj個を選んだとする
dp = [[0] * 4 for _ in range(sortedNumCount + 1)]
dp[0][0] = 1
for ind in range(1, sortedNumCount + 1):
    x = sortedNum[ind-1]
    dp[ind][0] = 1
    dp[ind][1] = dp[ind - 1][1] + dp[ind - 1][0] * cnt[x]
    dp[ind][2] = dp[ind - 1][2] + dp[ind - 1][1] * cnt[x]
    dp[ind][3] = dp[ind - 1][3] + dp[ind - 1][2] * cnt[x]
print(dp[sortedNumCount ][3])

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?