P2946 Cow Frisbee Team S
0.總結
這是一道好題 (我太菜了),這是一道好題(我太菜了),這是一道好題(我太菜了)!重要的話說三遍。
Get to the key point firstly, the article comes from LawsonAbs!
坑點如下:
- 數據範圍過大,導致無法開出2000*100000的數組,即樸素的0/1揹包無法解決問題
- 在把能力對F取模處理之後,利用一維的滾動數組會造成問題的錯誤解
- 答案要對 10^8 取模
下面正式進入主題
1.題意
給出兩個整數n,f。接下來輸入n個數,要求從這 n 個數中選出若干個數,它們組成的和能夠整除f,求這種選擇的個數。又因爲解的數過大,要求對10^8取模。
2.主要思路
一眼望去,滿屏都是 “0/1揹包”的身影。思路如下:
- 01.令
dp[i]
表示到達能力i
的選擇種類數。初始時dp[0] = 1
。 - 02.雙重
for
循環後,依次放入第1-n
個物品 【其下標對應分別是0 — n-1
】,倒序更新j的值,然後依次得到到達能力j的選擇種類。 - 03.最後再用一個for循環判斷每個能力值是否可以整除f。如果是累計答案,同時將答案取模。
- 04.輸出結果
res
.
興奮的10min編碼,但是卻只能得到 80分,【另外兩個分別是 RE,TLE】。於是找問題。
結合題目的數據範圍,可以看到
N(1≤N≤2000);Ri(1≤Ri≤100000);F(1≤F≤1000)
那麼能力值的範圍就是 2000 * 100000
。毫無疑問,開這麼大的數組肯定會MLE。那接下就要想該怎麼優化了。
題目要求能力值能整除f時,才作爲結果的一部分。根據加法模運算的規則【(a+b)%F = a%F + b%F
】那麼就可以考慮
是否不用把所有的能力加起來之後對F取模,而是在相加的同時就對F取模。
這樣最後只需要輸出 dp[0] - 1
【F!=1時】 或者dp[0]
【F=1時】 即可。舉例來說:
2 5
1
10
可以到達的能力是
dp[0]=1; dp[1]=1; dp[10]=1; dp[11] = 1;
其中符合輸出條件的只有10一個。而如果我們在對求出的能力值對F取餘再寫入數組時,就得到如下的結果
dp[0]=1; dp[1]=2; dp[10]=1;
dp[1]
由之前的1變成了2。但是結果並不會發生變化【不會發生變化的原因就是 加法模運算這個規則造成的】。所以現在可以確定的是:
- 利用模運算,將能力值範圍從
0- N*Ri
縮小到0-F
但是像上面說的這樣硬性取模真的可以解決問題嗎?再看一組測試用例:
6 5
1
3
10
2
8
2
輸入時對數據同時進行取餘操作,得到的樣例就變成:
6 5
1
3
0
2
3
2
dp的過程是:用上一輪的dp[j]
與 dp[j-arr[i]]
求和作爲本輪的dp[j]
,其中必有 j>arr[i] ,但是因爲我們對和進行了取模操作,就無法保證j>arr[i] 恆成立。比如10對5取模成了0,2對5取模成了2。0<2,但是10>2。所以直接用式子 dp[j] += dp[j-arr[i]]
求,就相當於是 dp[0] += dp[-2]
顯然是不合理的。所以需將操作變成 dp[j] += dp[(j-arr[i]+f)%f]
但這樣依然無法解決問題。因爲((j-arr[i]+f)%f)
這個操作會改變j與arr[i]之間的大小關係。原本j大於arr[i], 可能在操作之後, arr[i]大於j,就導致之前做的倒序枚舉失效,從而無法得到正確的結果,所以在此基礎上,應該使用2維dp,將上一次的結果值放在一個數組裏,而避免出現錯誤。
3.AC代碼
#include<iostream>
using namespace std;
const int maxN = 2005;
const int maxV = 1005;
int n,f;
int arr[maxN],dp[2][maxV];
int main(){
cin >> n >>f;
for(int i = 0 ;i< n;i++){
cin >>arr[i];
arr[i] %= f;
}
fill(dp[0],dp[0]+2*maxV,0);
//1.初始化,放第1個物品【即arr[0]達到的情況】
dp[0][0] = 1;
dp[0][arr[0]] = 1;
//2.開始依次將第2-(n-1)個物品放入
for(int i = 1;i< n;i++){
for(int j = f-1;j >= 0;j--){
dp[i&1][j] = dp[(i-1)&1][(j-arr[i]+f)%f] + dp[(i-1)&1][j];
dp[i&1][j] %= 100000000;
}
}
int ei = (n-1)&1;
int res = dp[ei][0];
if(f == 1)
cout << res<<"\n";
else
cout << res - 1<<"\n";
return 0;
}
4.測試用例
2 1
1
1
2
2 2
1
1
1
1 1
1
1
6 5
1
3
10
2
8
2
13
4 5
1
2
8
2
3