1. 程式人生 > >【做題】CSA49F - Card Collecting Game——思維&dp

【做題】CSA49F - Card Collecting Game——思維&dp

原文連結 https://www.cnblogs.com/cly-none/p/CSA49F.html

題意:Alice和Bob在玩遊戲。有\(n\)種卡牌,每種卡牌有\(b_i\)張,保證\(\sum b_i\)為偶數。現在,Alice要把所有卡牌任意平分為2份(僅要求每份卡牌數為\(\frac {\sum b_i} {2}\)),並對每份分別進行一次遊戲。第一次遊戲由Alice先手,第二次由Bob先手。

每次遊戲中,Alice和Bob會輪流取走一張卡牌直到取盡。設最後Alice有\(n_i\)張第\(i\)種牌,那麼她會得到\(\left\lfloor \frac {n_i} {a_i} \right\rfloor c_i\)

的分數。一次遊戲的得分是Alice從每種牌得到的分數總和。

現在,Alice想要最大化兩次遊戲的得分總和,Bob則想最小化。求出在兩人都採取最優決策時的得分總和。

\(n \leq 2\times 10^3, \ \sum a_i \leq 2 \times 10^3, \ \sum b_i \leq 5\times 10^5, \ c_i \geq 0\)

先考慮如何計算一次遊戲的得分。

首先,因為是輪流取,故對於每\(2a_i\)張卡牌\(i\),都能產生\(c_i\)的分數。因此,我們可以先考慮這一部分的貢獻,然後將所有\(n_i\)\(2a_i\)取模。

接下來,考慮如果\(n_i < 2a_i - 1\)

,那麼只要Bob跟著Alice取,Alice就得不到這個\(c_i\)。當\(n_i = 2a_i - 1\)時,先手就能恰好取到\(a_i\)張牌,但先後手順序會交換。

因此,對於剩下來的卡牌,我們忽略\(n_i < 2a_i - 1\)的,並對剩下的按\(c_i\)從大到小排序。那麼,若Alice先手,就能得到\(\sum_{2 \nmid i} c_i\)的分數;否則就是\(\sum_{2 | i} c_i\)

那麼,我們就能得到一個dp的做法。以\(c_i\)為關鍵字排序後,設\(dp[i,j,a,b]\)表示當前處理了前\(i\)種卡牌,第一份已經有\(j\)張卡牌,且第一份有\(a\)

\(n_i\)\(2a_i\)取模後是\(2a_i-1\)的卡牌,第二份有\(b\)種。注意到只要記錄\(a\)\(b\)的奇偶性就可以了。暴力轉移,則這個dp的複雜度是\(O((\sum b_i)^2)\)

但這樣還不足以解決本題。考慮\(\sum a_i\)比較小,故我們要從這個角度來優化dp。

先注意到兩點,一是我們在轉移時,產生的貢獻只和放到第一份的數量對\(2a_i\)取模的值有關;二是dp狀態中最龐大的\(j\),最後只是用來確定第一份的數量等於\(\frac {\sum b_i} {2}\)的。於是我們考慮對\(j\)進行優化,目的是在轉移時,只用列舉放在第一份的數量對\(2a_i\)取模的值。

於是我們把第一份卡牌分為兩個部分,一部分是所有\(n_i\)\(2a_i\)取模後的結果,則另一部分的卡牌總數就是\(\sum_{i} 2a_ik_i\)的形式。要保證第一份的卡牌總數為一個固定值,我們就要求出\(\sum_{i} 2a_i k_i\)的能表示出哪些數。

先考慮\(k_i\)的取值範圍。設\(n_i \mod 2a_i = r_i\),那麼\(k_i\)就是在\(\left[ 0, \left\lfloor \frac {b_i - r_i} {2a_i} \right\rfloor \right]\)之間的整數。但\(\left\lfloor \frac {b_i - r_i} {2a_i} \right\rfloor\)有兩種取值:在\(r_i \leq b_i \mod 2a_i\)時,為\(\left\lfloor \frac {b_i} {2a_i} \right\rfloor\);否則是\(\left\lfloor \frac {b_i} {2a_i} \right\rfloor - 1\)。於是我們不妨就令\(k_i\)的上界為\(\left\lfloor \frac {b_i} {2a_i} \right\rfloor - 1\),當\(r_i \leq b_i\)的時候,把多出來的那個\(2a_i\)算在第一部分裡就可以了。

剩下就是一個揹包問題。注意到我們可以把\(a_i\)相等的數放在一起計算,而\(a_i\)只有$ \sqrt {\sum a_i}\(種取值,因此這個問題的複雜度是\)O(\sqrt {\sum a_i} (\sum b_i))$的。

總結一下,第一部分的處理和\(O((\sum b_i)^2)\)的演算法差不多,但這一部分的複雜度是\(O((\sum a_i)^2)\)的。第二部分的揹包,複雜度為\(O(\sqrt {\sum a_i} (\sum b_i))\)

於是時間複雜度為\(O((\sum a_i)^2 + \sqrt {\sum a_i} (\sum b_i))\)

#include <bits/stdc++.h>
using namespace std;
const int A = 2010, B = 500010, N = 2010;
int dp[2][A << 2][2][2], bag[B], n, num[A], p, sa, sb, ans;
struct data {
  int a,b,c;
  bool operator < (const data& x) const {
    return c > x.c;
  }
} dat[N];
inline void ckmx(int& x,int y) {
  x = x < y ? y : x;
}
int main() {
  scanf("%d",&n);
  for (int i = 1 ; i <= n ; ++ i)
    scanf("%d%d%d",&dat[i].a, &dat[i].b, &dat[i].c);
  sort(dat+1,dat+n+1);
  for (int i = 1 ; i <= n ; ++ i) {
    if (dat[i].b / (dat[i].a << 1) - 1 > 0)
      num[dat[i].a] += dat[i].b / (dat[i].a << 1) - 1;
    sa += dat[i].a;
    sb += dat[i].b;
  }
  p = 1;
  memset(dp,-1,sizeof dp);
  dp[0][0][0][0] = 0;
  for (int i = 1, sum = 0 ; i <= n ; ++ i, p ^= 1) {
    memset(dp[p],-1,sizeof dp[p]);
    for (int j = 0 ; j <= (sum << 2) ; ++ j)
      for (int a = 0 ; a < 2 ; ++ a)
        for (int b = 0 ; b < 2 ; ++ b) {
          if (dp[p^1][j][a][b] == -1) continue;
          for (int k = 0 ; k < 2 * dat[i].a && k <= dat[i].b ; ++ k) {
            int tmp = (dat[i].b - k) / (dat[i].a << 1), na = a, nb = b;
            if (k == 2 * dat[i].a - 1) {
              if (!na) tmp ++;
              na ^= 1;
            }
            if ((dat[i].b - k) % (dat[i].a << 1) == (dat[i].a << 1) - 1) {
              if (nb) tmp ++;
              nb ^= 1;
            }
            tmp = tmp * dat[i].c;
            ckmx(dp[p][j + k][na][nb], dp[p^1][j][a][b] + tmp);
            if (dat[i].b >= 2 * dat[i].a && k <= dat[i].b % (dat[i].a << 1))
              ckmx(dp[p][j + k + 2 * dat[i].a][na][nb], dp[p^1][j][a][b] + tmp);
          }
        }
    sum += dat[i].a;
  }
  bag[0] = 1;
  for (int i = 1 ; i < A ; ++ i) {
    if (!num[i]) continue;
    for (int j = 0 ; j <= sb ; ++ j) {
      if (bag[j]) bag[j] = 0;
      else {
        bag[j] = -1;
        if (j >= 2 * i) {
          if (bag[j - 2 * i] != -1)
            bag[j] = bag[j - 2 * i] + 1;
        }
      }
    }
    for (int j = 0 ; j <= sb ; ++ j)
      if (bag[j] == -1) bag[j] = 0;
      else if (bag[j] <= num[i]) bag[j] = 1;
      else bag[j] = 0;
  }
  p ^= 1;
  for (int i = 0 ; i <= (sa << 2) && i <= (sb >> 1) ; ++ i)
    for (int a = 0 ; a < 2 ; ++ a)
      for (int b = 0 ; b < 2 ; ++ b) {
        if (dp[p][i][a][b] == -1) continue;
        if (bag[(sb >> 1) - i]) ans = max(ans, dp[p][i][a][b]);
      }
  printf("%d\n",ans);
  return 0;
}


小結:這個問題相當有難度。得到\(O((\sum b_i)^2)\)的dp已經偏難,而後一部分的優化對思維能力和細節處理能力的要求,是在博主目前能力之上的,也體現了演算法優化的一些重要思路。