Issei's Blog

【競プロ】Exam and Wizardを解く

こんにちは、モリリンです。
今日も今日とて、朝から競プロ解いていきます。
今日の問題はキーエンスプログラミングコンテスト2019のC問題、Exam and Wizard です。

問題文

大学生の高橋君は NN 個の試験を受けてすべてに合格する必要があります. 現在,ii 番目の試験の準備度は AiA_i です. また,高橋君の入念な調査によって,ii 番目の試験に合格するためには準備度を BiB_i 以上にしなくてはならないことが分かっています.
このままだとすべての試験に合格できないかもしれないと思った高橋君は,魔法使いの青木君に頼んで, 試験の準備度の総和は変えずに,なるべく少ない数の試験の準備度を変更してもらうことで試験を乗り切ることにしました.
高橋君に代わって,以下の条件を満たす数列 C1,C2,...,CNC_1,C_2,...,C_N を考えたときの AiA_iCiC_i が異なるような ii の個数の最小値を求めてください. そのような数列 C1,C2,...,CNC_1,C_2,...,C_N が構成できない場合は −1 を出力してください.

  • 数列 A1,A2,...,ANA_1,A_2,...,A_N の総和と数列 C1,C2,...,CNC_1,C_2,...,C_N の総和は等しい
  • どの ii に対しても,BiCiB_i \leq C_i が成り立つ

制約

1N1051Ai1091Bi1091 \leq N \leq 10^5 \\ 1 \leq A_i \leq 10^9 \\ 1 \leq B_i \leq 10^9 \\

ポイント

今回の問題の場合、Nの制約が10510^5と少し大きいため、O(N)O(N)、つまり、ループが一重くらいで計算できたらいいなーくらいの気持で考えます。

考えかた

まずは、入力例1をみていきます。
例1では、Ai=2,3,5,Bi=3,4,1{A_i} = {2,3,5}, {B_i} = {3,4,1} となっています。
準備度A1<B1A_1 < B_1 なので、A1A_1B1B_1 以上に変更する必要があり、最低 B1A1=32=1B_1 - A_1 = 3 - 2 = 1 を他のところから必要になります。 同じように A2<B2A_2 < B_2 なので、 A2A_2B2B_2 以上に変更するために最低 11 必要になります。
なので合計22を他の準備度からもらってくる必要があります。
そして、すでにA3>B3A_3 > B_3 となっているので、A3A_3 を変更して、余った準備度A3B3=4A_3 - B_3 = 4をそれらの足りていないところに割り当てることができます。
なので、結局全ての準備度を変更する必要があるので、答えは3となります。

つまり、Ai<BiA_i < B_i となっているiiについて、BiAiB_i - A_iの合計分が条件を満たすのに必要で、その分をすでにAi>BiA_i > B_i となっているところから AiA_i を変更して、余った準備度 AiBiA_i - B_i を割り当てればいいことになります。
そして、変更回数を最小にするために、AiBiA_i - B_i が大きなところから変更して、余った準備度を他に割り当ていきます。

そして求める答えは、
Ai<BiA_i < B_i となっているiiについての、AiBiA_i - B_iの合計をreq
Ai>BiA_i > B_i となっているii について、AiBiA_i - B_iを並べたものを extras
その合計をs_extraとしたとき、
req>sextrareq > s_extra なら答えは -1
それ以外の時、答えは (Ai<BiA_i < B_i となっているiiの合計) + (extrasの要素を大きい順から足していって何要素目に reqを超えるか)
になります

実装

上記のことを実装するには、まず、
req, s_extra, extras を宣言します。
なお、 resultは答えを表します。 そして、0i<N0 \leq i < N について、

  • A[i]>B[i]A[i] > B[i]なら、s_extraA[i]B[i]A[i] - B[i]を足し、extrasA[i]B[i]A[i] - B[i]を追加
  • A[i]<B[i]A[i] < B[i]なら、reqA[i]B[i]A[i] - B[i]を足し、resultに1を足す

そして、extrasを降順にソートして、

extrasから要素を1つづつ取り出していって、

  • reqが0以下なら終了
  • それ以外なら、
    • reqをその値で引き
    • resultに1を足す

を繰り返します

Nimで書いたコードは以下になります。

import sequtils, strutils, sugar, algorithm
proc scanf(formatstr: cstring){.header: "<stdio.h>", varargs.}
proc getchar(): char {.header: "<stdio.h>", varargs.}
proc nextInt(): int = scanf("%lld", addr result)
proc nextFloat(): float = scanf("%lf", addr result)
proc nextString(): string =
  var get = false
  result = ""
  while true:
    var c = getchar()
    if int(c) > int(' '):
      get = true
      result.add(c)
    else:
      if get: break
      get = false

proc `$` [T](x: seq[T]): string = x.mapIt($it).join(" ")


proc solve(N: int, A: seq[int], B: seq[int]): int =
  var req, s_extra = 0
  var extras: seq[int]

  for i in 0..<N:
    if A[i] > B[i]:
      s_extra += A[i] - B[i]
      extras.add(A[i] - B[i])
    elif A[i] < B[i]:
      req += B[i] - A[i]
      inc(result)
  if req > s_extra:
    return -1
  extras.sort(Descending)
  for extra in extras:
    if req <= 0:
      return result
    req -= extra
    inc(result)


proc main(): void =
  var N = 0
  N = nextInt()
  var A = newSeqWith(N, 0)
  for i in 0..<N:
    A[i] = nextInt()
  var B = newSeqWith(N, 0)
  for i in 0..<N:
    B[i] = nextInt()
  echo solve(N, A, B)
  return

main()

提出結果

終わりに

いかがだったでしょうか?
今回のような問題は、実際に入力例などを手で描いてみて、自分ならどうやって解くのかを考え、それをコードに落としていく、みたいなことをやるとうまくいくと思います。