測試地址:找硬幣
做法: 本題需要用到DP+數論。
假設我們有了構造出了一個合法硬幣序列x,怎麼計算最少需要使用的硬幣數量?顯然,因爲xk爲xk−1的倍數,能用大的就應該用大的,那麼對於最大的幣值xk,應該要使用⌊xkai⌋個,於是還剩下ai%xk需要支付,於是對於第二大的幣值xk−1需要使用⌊xk−1ai%xk⌋個,於是還剩下ai%xk−1需要支付…以此類推。於是我們得到答案:
ans=∑i=1n(⌊xkai⌋+∑j=1k−1⌊xjai%xj+1⌋)
我們發現xj之間互相產生貢獻,會且只會在相鄰的xj和xj+1之間,以及最後再補上一個⌊xkai⌋,也就是說xj的選取是一個可以多階段決策的問題,也就可以用動態規劃解決了。
爲了方便,我們先把⌊xkai⌋那個部分略掉(因爲可以O(n⋅maxai)算出),令f(p)爲xk=p時ans的最小值,那麼有狀態轉移方程:
f(x)=min{f(y)+∑i=1n⌊yai%x⌋}(y∣x)
邊界爲f(1)=0。這個東西每次轉移都暴力計算是O(n)的,而鑑於從f(p)可以轉移到p的所有的倍數,因此轉移次數是一個調和級數,也就是O(maxai⋅log(maxai))次轉移,那麼總的時間複雜度爲O(n⋅maxai⋅log(maxai)),爆炸的可能性很大(我沒試過,但應該會掛)。
於是我們需要找到O(1)轉移的方法,唯一的方式只有預處理出一部分答案,而上面那個∑i=1n⌊yai%x⌋實在有點糾結,我們考慮怎麼把⌊ya%x⌋轉化成更好算的式子。
直覺上,我們感覺到⌊ya%x⌋=⌊ya⌋%yx,注意此處y∣x,這是一個非常重要的性質。簡單證明如下:
根據等式的左邊,可以令a=k1x+r1(0≤r1<x),則r1=a%x,而令r1=k2y+r2(0≤r2<y),則k2=⌊ya%x⌋。
將a=k1x+k2y+r2代入等式的右邊,那麼⌊ya⌋=k1⋅yx+k2,而k2=⌊yr1⌋,r1<x,於是k2<yx,因此k2也=⌊ya⌋%yx,等式兩邊都等於k2,所以等式成立。
然後⌊ya⌋%yx=⌊ya⌋−⌊yx⌊ya⌋⌋⋅yx,因爲yx是正整數,根據一個用過很多次我不想再證的結論⌊n⌊ma⌋⌋=⌊mna⌋,上式就可以寫成⌊ya⌋−⌊xa⌋⋅yx。有了這一結論,帶回一開始的式子中去,則有:
∑i=1n⌊yai%x⌋=∑i=1n(⌊yai⌋−⌊xai⌋⋅yx)=(∑i=1n⌊yai⌋)−yx⋅(∑i=1n⌊xai⌋)
令g(x)=∑i=1n⌊xai⌋,則狀態轉移方程就可以寫成:
f(x)=min{f(y)+g(y)−yx⋅g(x)}(y∣x)
而g(x)可以O(n⋅maxai)預處理,於是轉移就是O(1)的了,總的複雜度就是O(n⋅maxai+maxai⋅log(maxai)),可以輕易地通過此題。
以下是本人代碼:
#include <bits/stdc++.h>
using namespace std;
const int inf=1000000000;
int n,a[55],maxa=0,tot[100010],f[100010],ans=inf;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
maxa=max(maxa,a[i]);
}
for(int i=1;i<=maxa;i++)
{
f[i]=inf;
tot[i]=0;
for(int j=1;j<=n;j++)
tot[i]+=a[j]/i;
}
f[1]=0;
for(int i=1;i<=maxa;i++)
{
for(int k=2;i*k<=maxa;k++)
f[i*k]=min(f[i*k],f[i]+tot[i]-tot[i*k]*k);
ans=min(ans,f[i]+tot[i]);
}
printf("%d",ans);
return 0;
}