<think>我们首先需要理解题目:洛谷P5228 [AHOI2013]找硬币
题目描述:小蛇在购买物品时,希望使用最少的硬币数。他拥有的硬币面值由他决定(但必须是正整数,且最小面值为1)。现在给定n个物品的价格(均为正整数),设计一组硬币面值,使得支付所有物品所需的总硬币数最少。
分析:
1. 题目要求我们设计一套硬币系统,使得支付给定的n个价格时,使用的总硬币数最少。
2. 注意:硬币系统必须包含面值1(因为最小面值为1),并且硬币面值都是正整数。
3. 支付每个物品时,可以任意组合硬币,但必须恰好等于物品价格。
观察:题目中样例:两只兔纸,价格分别为25和102。最优解是设计面值为1,25,100的硬币系统。支付25:1个25;支付102:1个100和2个1,共1+3=4个硬币。
思路:
1. 动态规划。由于面值的设计是递增的,并且最大的面值不会超过所有物品价格的最大值(实际上,最大面值可以设到max(ai)即可,但为了更优,可能更大?然而题目并没有限制面值上限,但显然我们不会设计超过max(ai)的面值,因为更大的面值无法用于支付任何物品(除非恰好等于某个物品价格,但我们可以用更小的组合)?然而,实际上,我们可能会设计超过最大价格的面值吗?不会,因为超过最大价格的面值无法使用(因为每个物品支付必须恰好等于价格),所以面值最大为max(ai)即可。
2. 但是,考虑样例:两个物品,价格分别为1和3。如果我们设计面值1和3,那么支付1用1个1,支付3用1个3,总共2个硬币。如果我们只设计1,那么支付3需要3个1,总共4个硬币。所以设计面值3是必要的。然而,如果我们设计面值1和2,那么支付1:1个1,支付3:1个2和1个1,总共3个硬币。所以最优是设计1和3。
3. 因此,我们需要选择一组面值,使得总硬币数最少。面值必须包含1,且是递增的。设面值序列为v1=1, v2, v3, ..., vk(v1<v2<...<vk)。
4. 如何计算在给定面值系统下,支付一个价格a所需的硬币数?我们可以用贪心(因为面值系统是我们设计的,我们可以要求它满足倍数的关系吗?)注意:题目没有要求硬币系统满足“整除”关系(即相邻面值不一定成倍数),但是,如果我们设计的系统不是倍数关系,那么贪心可能不是最优的(例如,如果面值1,3,4,那么支付6:用两个3是最优,而不是4+1+1)。然而,题目要求的是我们设计面值系统,然后支付时使用任意组合(即不限于贪心,可以任意组合),所以对于给定的面值系统,支付a的最小硬币数就是完全背包的最小硬币数。
5. 但是,如果我们设计的面值系统是任意的,那么对于每个物品我们都要用完全背包来计算最小硬币数,这样计算量会很大(因为物品数n和价格最大为M,而面值系统最多有M种面值,那么完全背包的复杂度为O(n*M^2))。而且,我们还要枚举面值系统,这显然不可行。
6. 因此,我们需要更优的解法。参考题解,通常有如下性质:
- 最优解的面值序列一定是倍数递增的,即v_{i+1}是v_i的倍数(或者至少是倍数关系)。为什么?因为如果不是倍数关系,那么我们可以通过调整来减少总硬币数。例如,假设我们有两个面值a和b,且a<b,但b不是a的倍数。那么存在某个价格x,用a和b支付时,可能会因为b不是a的倍数而导致需要更多的a。而如果我们把b调整为a的倍数(比如调整为大于等于当前b的最小a的倍数),那么支付任何价格所需的硬币数不会增加(因为原来使用的b现在可以用若干个a来替代,但调整后新的b可能更大,所以可能减少硬币数)。实际上,这个性质在经典的最小硬币系统设计问题中是成立的(即最优解的面值系统是倍数关系)。
7. 因此,我们可以假设面值系统是倍数递增的:即v1=1, v2=k1*v1, v3=k2*v2, ...,其中ki>=2。
8. 这样,面值系统就可以用一个序列k1, k2, ...来描述。同时,面值系统由这个序列唯一确定(v1=1, v2=k1, v3=k1*k2, ...)。那么,我们只需要枚举这些倍数序列即可。
9. 但是,倍数序列的长度(即硬币种类的数量)最多为log(max(ai))(以最小倍数2递增,最大面值不超过max(ai)),所以最多大约20种(因为2^20>10^6,而题目中ai<=10^5)。
10. 那么,如何计算在给定倍数关系的面值系统下,支付所有物品所需的总硬币数?
对于每个物品价格a,我们使用贪心(因为面值系统是倍数关系,所以贪心是最优的):从大到小使用面值,对于面值v,使用c = a / v 个硬币,然后a %= v,直到a=0。
11. 因此,我们可以用动态规划来枚举倍数序列。但是,倍数序列的乘积(即最大面值)不能超过max(ai)(因为超过max(ai)的面值不会被使用,而且我们设计的面值最大为max(ai)即可,但为了计算方便,我们可以让最大面值达到max(ai))。
12. 状态设计:设dp[i]表示当前最大面值为i时,支付所有物品所需的总硬币数的最小值。注意:我们设计的面值系统必须包含1,且下一个面值必须是当前面值的倍数。
13. 转移:我们考虑在现有面值系统上增加一个新的面值,它是当前最大面值的倍数。设当前最大面值为v,下一个面值为k*v(k>=2)。那么,新的面值系统为原来的面值加上k*v。
14. 但是,增加新面值后,每个物品的支付方式可以优化:原来用多个v支付的,现在可以用一个k*v支付(或者用更少的大面值)。具体来说,对于每个物品价格a,原来在面值系统最大为v时,我们使用硬币数为c1(即贪心计算的结果)。现在增加了k*v后,我们可以将a分解成:a = (a // (k*v)) * (k*v) + a % (k*v),而a%(k*v)部分用原来的面值系统(最大面值为v)来支付。因此,在增加了面值k*v后,支付a的硬币数减少量为:原来支付a时,在面值v这一层使用了多少个v?记为count_v。现在,我们可以用面值k*v来替代k个v(注意:k*v相当于1个硬币,而k个v需要k个硬币),所以节省的硬币数为:count_v - (count_v // k) ??? 实际上,并不是这样简单。
15. 重新思考:在增加面值k*v后,支付a的硬币数可以这样计算:
- 使用c1个k*v(c1 = a / (k*v)),剩余a1 = a % (k*v)
- 然后对于a1,使用面值系统(最大面值为v)来支付,设需要c2个硬币。
所以总硬币数为c1 + c2。
而原来没有k*v时,支付a需要:c3 = a / v + (用更小的面值支付a%v的硬币数) ?注意,原来最大面值是v,所以支付a:先使用a/v个v,然后剩余a%v用更小的面值支付。设更小的面值支付a%v需要c4个硬币,那么原来总硬币数为a/v + c4。
但是,在增加k*v后,我们支付a时,在v这一层使用的硬币数不再是a/v,而是a1/v(其中a1=a%(k*v)),所以节省的硬币数实际上是:(a/v) - (a1/v) (因为a/v - a1/v = (a - a1)/v = (k*v*c1)/v = k*c1)?不对,因为a - a1 = k*v*c1,所以(a/v) - (a1/v) = (a - a1)/v = (k*v*c1)/v = k*c1。
因此,原来在v这一层使用的硬币数是a/v,现在在v这一层使用的硬币数是a1/v,节省了k*c1个硬币?但同时,我们增加了c1个硬币(因为使用了c1个k*v)。所以净节省:k*c1 - c1 = (k-1)*c1。
所以,增加面值k*v后,支付a的硬币数减少量为(k-1)*c1。
那么,总节省量就是所有物品的(k-1)*c1之和。
因此,如果我们记f(v)为当前最大面值为v时支付所有物品的总硬币数,那么增加面值k*v后,总硬币数变为:
f(k*v) = f(v) - (k-1) * (所有物品的c1之和) [其中c1是物品a在增加新面值k*v后,使用的k*v的个数,即a/(k*v)]
16. 因此,我们可以这样设计DP:
- 状态:dp[i]表示当最大面值为i时,支付所有物品所需的最小总硬币数。
- 初始化:dp[1] = 所有物品价格之和(因为只有面值1,所以每个价格a需要a个硬币)。
- 转移:对于每个i(从1到max(ai)),枚举倍数k(k>=2),使得j=k*i<=max(ai)(实际上,我们可以让j超过max(ai)吗?不行,因为超过max(ai)的面值不会被使用,所以j最大到max(ai)即可)。然后:
dp[j] = min(dp[j], dp[i] - (k-1)*s)
其中s是所有物品的a除以j的商之和,即s = sum_{a in A} (a // j)
- 但是,这样转移是否正确?我们考虑每个物品a,在最大面值为i时,支付a的硬币数是多少?实际上,在最大面值为i时,我们支付a的硬币数并不是一个固定的值,因为面值系统是包含1到i的所有倍数关系的面值(即1, k1, k1*k2, ... , i),所以支付a的硬币数应该是贪心计算的结果。但是,我们上面的推导是基于“增加新面值j=k*i”后,节省的硬币数等于(k-1)*s。这个推导成立的前提是:在最大面值为i时,支付a的硬币数等于(a//i + 用更小面值支付a%i的硬币数)。而更小面值支付a%i的硬币数,实际上在最大面值为i时已经是最优的(因为面值系统是倍数关系,贪心成立)。
17. 然而,我们并没有直接计算dp[i]的具体值(除了dp[1]),而是通过dp[i]去更新dp[j](j=k*i)。那么,我们如何保证dp[i]的值是正确的?实际上,我们是从最小的面值开始(dp[1]已知),然后逐步扩展更大的面值。由于倍数关系,每个面值系统都会被枚举到。
18. 但是,注意:面值系统可能不是连续的倍数关系?比如,我们可能跳过一些倍数?实际上,我们枚举的倍数k>=2,所以下一个面值一定是当前面值的倍数,并且我们从小到大枚举,所以可以覆盖所有倍数关系的面值系统。
19. 因此,算法步骤:
- 设max_a = max(ai),即所有物品的最大价格。
- 初始化dp数组:dp[1] = sum(ai) (因为只有面值1)
- 然后,对于每个i从1到max_a,如果dp[i]已经被计算过(即不是无穷大),则枚举倍数k(k>=2),计算j=k*i,如果j<=max_a,则:
s = 0
for each a in A:
s += a // j
然后,dp[j] = min(dp[j], dp[i] - (k-1)*s)
- 最后,答案就是所有dp[i](i从1到max_a)的最小值。
20. 为什么?因为dp[i]表示最大面值为i时的总硬币数,而我们的面值系统可以包含多个面值(1, i1, i2, ..., i),但注意,我们DP的过程是逐步增加面值的,并且每次增加的面值都是当前最大面值的倍数,所以整个面值系统是倍数递增的。而且,我们枚举了所有可能的最大面值(从1到max_a),所以最优解一定在其中。
21. 但是,这样设计的状态转移是否覆盖了所有可能的倍数序列?比如,面值系统可能是1,2,4,8,也可能是1,2,6(如果2*3=6)?注意,我们的转移是:当前面值为i,然后我们枚举一个倍数k,得到下一个面值j=k*i。那么,当我们处理i=2时,枚举k=3,得到j=6,这样面值系统就包含了1,2,6。然后,当处理i=6时,还可以再枚举k=2,得到j=12,等等。所以,所有倍数序列都被枚举了。
22. 然而,我们并没有考虑面值系统的中间面值?比如,我们可能同时有面值2和3,然后下一个面值6(既是2的倍数又是3的倍数)。但是,我们的DP转移是线性的:从1开始,然后1->2,1->3,然后2->4,2->6,3->6,3->9等等。那么,当我们计算dp[6]时,可能从dp[2]转移过来(k=3),也可能从dp[3]转移过来(k=2)。但是,在dp[2]转移时,我们只考虑了面值系统为1,2,6,而忽略了3?实际上,在dp[2]时,我们增加面值6,此时面值系统是1,2,6,中间没有3。那么,在支付时,对于a%6的部分,我们使用面值1和2来支付(因为最大面值2),所以这部分支付是使用1和2的最优解(贪心)。但是,如果我们有面值3,可能支付a%6时更优?然而,我们的面值系统是倍数关系,所以1,2,6这个系统并不包含3,所以不能使用3。因此,我们的DP状态dp[i]只表示当前最大面值为i,并且面值系统是由转移路径上的那些面值组成的(即1, ..., i,且i是前面某个面值的倍数)。
23. 但是,我们并没有要求面值系统是连续的倍数,比如1,2,4,8,中间可以跳过一些倍数吗?实际上,我们每次增加一个面值j=k*i,这个j是当前最大面值i的倍数,但和之前的面值不一定连续倍数(例如,我们可以有1,3,然后6=3*2,那么面值系统为1,3,6)。而1和3之间,3是1的3倍,6是3的2倍,所以整个系统还是倍数关系(相邻面值倍数)。
24. 所以,我们枚举了所有可能的倍数序列。但是,注意:同一个面值系统可能通过不同的路径得到。例如,面值系统1,2,4:
路径1:1->2(k=2)->4(k=2)
路径2:1->4(k=4) [这样在1的基础上直接跳到4,中间没有2]
那么,哪种路径正确?实际上,两种路径都是合法的,但是我们需要计算在面值系统1,2,4下的总硬币数,而路径1会计算出dp[2]和dp[4],路径2会直接计算出dp[4]。那么,哪种路径计算出的dp[4]更小?实际上,路径1:先计算dp[2]=dp[1] - (2-1)*s1,其中s1是所有a//2的和。然后dp[4]=dp[2] - (2-1)*s2,其中s2是所有a//4的和。而路径2:dp[4]=dp[1] - (4-1)*s2。那么,哪种更优?我们需要具体计算。
例如,考虑一个物品a=3:
路径1:dp[1]=3
计算dp[2]: 在增加面值2后,s1=3//2=1,所以dp[2]=3-1=2(因为节省了1个硬币:原来用3个1,现在用1个2和1个1,共2个硬币)
然后增加面值4:s2=3//4=0,所以dp[4]=2-0=2
路径2:dp[4]=3 - (4-1)*0=3
所以路径1更优。
因此,我们需要通过中间面值逐步转移,才能得到更优的解。所以,我们的DP转移中,必须枚举每一个倍数,并且从小到大地更新。
25. 但是,路径1和路径2都是合法的,我们都要枚举。所以,在DP中,我们对于每个i,枚举所有倍数k,然后更新dp[j](j=k*i)。这样,路径1和路径2都会被枚举到。
26. 然而,直接枚举倍数k,k从2到max_a//i,那么j=k*i<=max_a。这样,状态总数是max_a(因为i从1到max_a),每个状态枚举倍数k,总枚举量是O(max_a * (max_a/i)),对所有i求和,即O(max_a * H_{max_a}),其中H是调和级数,所以总复杂度O(max_a * log(max_a))。而max_a<=10^5,所以10^5*log2(10^5)≈10^5*17≈1.7e6,可以接受。
27. 但是,我们还需要计算s=sum(a//j),其中j=k*i。如果对每个j(即每个状态转移)都遍历所有a(n个物品)来计算s,那么总复杂度是O(n * 状态转移次数)=O(n * max_a * log(max_a)),而n<=50,max_a=10^5,那么50*1.7e6=85e6,在C++中可能勉强通过(注意常数优化)。
28. 优化:我们可以预处理每个j对应的s(j)=sum_{a in A} (a//j)。但是,j的范围是1到max_a(10^5),而n=50,所以我们可以用O(n*max_a)的预处理?不行,O(n*max_a)=50*10^5=5e6,可以接受。但是,我们不需要对每个j都计算,因为j只会在状态转移时出现(即j是某个i的倍数,且j<=max_a),但j的取值也是1到max_a,所以我们可以预先计算所有j的s(j)。
如何计算s(j)=sum(a//j)?对于每个j,我们遍历n个物品,计算a_i//j然后求和。总复杂度O(n*max_a)=50*10^5=5e6,可以。
29. 但是,注意:我们枚举的j=k*i,而i和k都是整数,j从1到max_a,所以我们可以预先计算好所有j(1到max_a)的s(j)。
30. 因此,算法步骤:
- 读入n和物品价格数组A,令max_a = max(A)
- 初始化dp数组(大小为max_a+1,下标从1到max_a),dp[1]=sum(A),其他dp[i]=无穷大(一个很大的数)
- 预处理数组s:s[j] = 0 for j in [1, max_a]
for j from 1 to max_a:
for each a in A:
s[j] += a // j (注意:整数除法)
- 但是,这样预处理需要O(n*max_a)时间,n=50,max_a=10^5,50*10^5=5e6,可以接受。
- 然后,对于i从1到max_a:
如果dp[i]是无穷大,跳过。
否则,枚举倍数k:k>=2,且j=k*i<=max_a:
dp[j] = min(dp[j], dp[i] - (k-1)*s[j])
- 最后,ans = min{ dp[i] for i in 1..max_a }
31. 但是,这里有一个问题:在状态转移时,我们使用的s[j]是固定的,与转移路径无关。而实际上,在dp[i]的状态下,面值系统是1,...,i(倍数关系),那么支付a时,小于等于i的部分已经用最优方式(贪心)支付了。当我们增加面值j后,支付a时,我们使用a//j个j,然后剩余a%j,而a%j部分用面值系统1..i来支付。但是,我们的s[j]只是a//j的和,并没有考虑剩余部分在面值系统1..i下的支付情况。然而,在dp[i]中已经包含了支付整个a的硬币数(即用1..i的面值系统支付a的硬币数)。而我们推导的节省量是(k-1)*s[j],这个节省量正是从原来支付a的硬币数中扣除的(因为原来在面值i下,支付a需要将a分解成若干i和剩余部分,而增加j后,原本用k个i支付的部分(k*i)现在可以用一个j支付,所以节省了k-1个硬币,而这里k是倍数,即j=i*k,所以一个j相当于k个i,所以节省k-1个硬币,而每个物品节省的硬币数就是(a//j)*(k-1)?不对,应该是(a//j)*(k-1)?不对,重新推导:
设当前最大面值为i,支付a的硬币数为:c = a//i + c',其中c'是用更小面值支付a%i的硬币数。
当我们增加面值j=k*i后,支付a变为:使用c1 = a//j个j,然后剩余a1=a%j,然后用最大面值i支付a1:需要c2 = a1//i + c''(其中c''是更小面值支付a1%i的硬币数)。
而原来支付a时,在i这一层使用的硬币数是a//i,现在支付a1时,在i这一层使用的硬币数是a1//i。
所以,在i这一层节省的硬币数为:a//i - a1//i = (a - a1) / i (因为a-a1是j的倍数,即k*i的倍数,所以(a-a1)/i = k * (a//j))
同时,我们增加了a//j个硬币(j面值的硬币)。
所以,净节省:k*(a//j) - (a//j) = (k-1)*(a//j)
因此,总节省量就是(k-1)*s[j](s[j]=sum(a//j))。
所以,转移公式正确。
32. 但是,注意:在增加面值j后,剩余部分a1的支付使用的是面值系统1..i(即最大面值为i),所以dp[j] = dp[i] - (k-1)*s[j] 就表示在原来dp[i]的基础上,节省了(k-1)*s[j]个硬币。
33. 因此,代码实现:
- 注意:dp数组初始化,dp[1]=所有a_i之和。
- 预处理s数组:s[j] = sum_{i=1}^{n} (a_i // j)
- 然后i从1到max_a循环,如果dp[i]不是无穷大,则枚举倍数k(从2开始,直到j=k*i<=max_a),更新dp[j] = min(dp[j], dp[i] - (k-1)*s[j])
34. 最后,答案取所有dp[i]的最小值(包括dp[1])。
35. 注意:有可能最优解就是dp[1](即只使用面值1),或者中间某个dp[i]更小。
36. 但是,我们这样得到的dp[i]对应的面值系统并不一定是完整的倍数序列?比如,dp[6]可能是从dp[3]转移过来的(k=2),此时面值系统是1,3,6(即1,3,6,中间没有2)。那么,在支付时,我们使用面值1,3,6,这样支付一个价格a时,我们使用贪心:先尽可能用6,然后用3,最后用1。所以,dp[6]的值就是使用面值系统{1,3,6}支付所有物品的总硬币数。
37. 因此,我们考虑的面值系统是:从1开始,每次乘以一个倍数k,得到下一个面值,直到最大面值不超过max_a。并且,每个状态dp[i]对应的面值系统就是由转移路径上的面值组成的(即从1开始,乘以一系列倍数得到i)。
38. 所以,我们枚举了所有可能的倍数序列(即所有可能的硬币系统),并且计算了总硬币数,取最小值。
39. 代码实现细节:
- 使用数组dp,大小为max_a+1(下标0不用,从1到max_a)。
- 初始化dp[1]=sum,其他dp[i]=INF(一个很大的整数,比如0x3f3f3f3f,或者1e18)。
- 预处理s数组:s[j] = 0; for (int i=1; i<=n; i++) s[j] += a[i]/j;
- 循环:for (int i=1; i<=max_a; i++) {
if (dp[i] == INF) continue;
for (int k=2; k*i<=max_a; k++) {
int j = k * i;
dp[j] = min(dp[j], dp[i] - (k-1)*s[j]);
}
}
- 然后ans = *min_element(dp+1, dp+max_a+1);
40. 但是,注意:我们枚举的倍数k,k从2开始,直到j<=max_a。那么,对于每个i,k的上限是max_a//i。
41. 最后输出ans。
42. 验证样例:两个物品,25和102,max_a=102。
dp[1] = 25+102=127
然后,我们枚举i=1,倍数k从2到102:
比如k=2,j=2,s[2]=25//2+102//2=12+51=63,则dp[2]=min(INF, 127-1*63)=64
k=3,j=3,s[3]=25//3+102//3=8+34=42,dp[3]=127-2*42=127-84=43
... 等等,直到k=25,j=25,s[25]=25//25+102//25=1+4=5,dp[25]=127-24*5=127-120=7
k=102, j=102, s[102]=0+1=1,dp[102]=127-101*1=26
然后,当i=25时,可以枚举k=4(因为25*4=100<=102),则j=100,s[100]=25//100+102//100=0+1=1,则dp[100]=min(INF, dp[25] - (4-1)*1) = 7-3=4
然后,当i=100时,枚举k=2,j=200>102,所以不枚举。
最后,ans=min(127,64,43,...,7,26,...,4)=4,符合样例。
43. 因此,代码实现如下:
注意:题目要求C++代码,并且注意效率(n<=50,max_a<=10^5,所以预处理s数组需要5e6次运算,DP部分需要O(max_a*log(max_a))≈1.7e6次循环,总运算次数约6.7e6,在C++中可接受)。
代码:
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
int max_a = 0;
long long sum = 0;
for (int i = 0; i < n; i++) {
cin >> a[i];
max_a = max(max_a, a[i]);
sum += a[i];
}
// 预处理s: s[i] = sum_{j=0}^{n-1} (a[j] // i)
vector<long long> s(max_a+1, 0); // s[0]不用,下标1..max_a
for (int i = 1; i <= max_a; i++) {
for (int j = 0; j < n; j++) {
s[i] += a[j] / i; // 整数除法
}
}
// dp数组,dp[i]表示最大面值为i时的最小总硬币数
vector<long long> dp(max_a+1, LLONG_MAX);
dp[1] = sum; // 初始:只有面值1
// DP
for (int i = 1; i <= max_a; i++) {
if (dp[i] == LLONG_MAX) continue; // 如果当前状态不可达,跳过
// 枚举倍数k,下一个面值j=k*i
for (int k = 2; (long long)k * i <= max_a; k++) { // 注意:k*i可能溢出int?但max_a<=10^5,k最大为max_a,所以k*i最大10^10,可能超过int,所以用long long转换,或者i用long long?这里k*i<=max_a,所以k<=max_a/i,所以k*i<=max_a,不会超过max_a(int范围内)
int j = k * i;
if (j > max_a) break; // 其实循环条件已经保证了,可以不要
// 转移: dp[j] = min(dp[j], dp[i] - (k-1)*s[j])
if (dp[i] - (k-1)*s[j] < dp[j]) {
dp[j] = dp[i] - (k-1)*s[j];
}
}
}
// 答案:所有dp[i]的最小值
long long ans = *min_element(dp.begin()+1, dp.end());
cout << ans << endl;
return 0;
}
44. 注意:上面的dp数组初始化时,dp[1]=sum,然后其他状态从dp[1]转移。但是,有可能最优解就是dp[1],所以最后取最小值时包括dp[1]。
45. 但是,我们只枚举了最大面值不超过max_a的情况,而题目没有要求最大面值不超过max_a?但是,超过max_a的面值不会被使用(因为每个物品的价格<=max_a),所以最大面值超过max_a没有意义(因为不会被使用,而且设计面值超过max_a,那么支付时不会使用,所以总硬币数不会减少)。因此,最大面值只需枚举到max_a。
46. 提交洛谷测试。
但是,注意:题目中每个物品的价格可能为0?题目说“正整数”,所以不用考虑0。
47. 优化:预处理s数组时,我们使用了二重循环,内层循环n,外层循环max_a,总循环次数n*max_a=50*10^5=5e6,在C++中可接受(约0.5秒)。但也可以优化:使用数论分块优化s数组的计算?但是n很小,所以不需要。
48. 另一种优化:我们注意到,对于j很大时,a[j]//j可能为0(当j>a[i]时,a[i]//j=0),所以我们可以对每个a[i],只枚举j从1到a[i](或者1到min(max_a, a[i])),这样总循环次数为sum_{i} a[i] <= n * max_a = 50*10^5=5e6,和上面一样。但是,我们上面的写法是枚举j,然后枚举每个a[i],这样在j>max_a时不会进入,所以和优化后一样。因此,不需要改变。
49. 运行测试:样例输入
2
25 102
输出:4
50. 另一个样例:两个物品,1和3,max_a=3。
dp[1]=1+3=4
预处理s:
s[1]=1+3=4
s[2]=0+1=1 (1//2=0, 3//2=1)
s[3]=0+1=1
然后,i=1时:
k=2: j=2, dp[2]=4-1*1=3
k=3: j=3, dp[3]=4-2*1=2
然后,i=2时:枚举k=2,j=4>3,跳过。
i=3时:枚举k=2,j=6>3,跳过。
最后,ans=min(4,3,2)=2,正确。
51. 因此,代码正确。
注意:由于题目中n最大50,max_a最大100000,所以预处理s数组需要50*100000=5000000次运算,在C++中大约0.5秒(一般1e6次运算约0.1秒,5e6约0.5秒)。DP部分状态数max_a=100000,每个状态枚举倍数k,总循环次数约为sum_{i=1}^{max_a} (max_a/i) 的约数个数和,即O(max_a * log(max_a)),100000*log2(100000)≈100000*17=1.7e6,所以总循环次数约1.7e6,也是0.1秒左右。总时间0.6秒,可以接受。
提交洛谷P5228。
但是,注意:洛谷P5228的题目名称是[AHOI2013]找硬币,和BZOJ3233是同一题。
最后,输出代码。</think>### 洛谷 P5228 [AHOI2013]找硬币 题解
#### 问题分析
题目要求设计一套硬币系统(必须包含面值1),使得支付给定物品价格时使用的总硬币数最小。硬币面值必须是正整数且递增,支付时使用任意组合。核心思路是利用动态规划,枚举所有可能的倍数关系面值系统,并计算对应的总硬币数。
#### 算法思路
1. **问题性质**:最优解的面值序列满足倍数递增关系($v_{i+1} = k_i \times v_i$,$k_i \geq 2$),可减少状态空间。
2. **状态定义**:设 $dp[i]$ 表示最大面值为 $i$ 时支付所有物品的最小总硬币数。
3. **状态转移**:
- 初始化:$dp[1] = \sum \text{所有物品价格}$(仅使用面值1)。
- 转移方程:$dp[j] = \min(dp[j], dp[i] - (k-1) \times s[j])$,其中 $j = k \times i$,$s[j] = \sum \lfloor a / j \rfloor$($a$ 为物品价格)。
4. **预处理**:计算 $s[j]$(所有物品价格整除 $j$ 的和)。
5. **答案**:$\min_{i=1}^{\text{max}\_a} dp[i]$。
#### 时间复杂度
- 预处理 $s[j]$:$O(n \times \text{max}\_a)$,$n \leq 50$,$\text{max}\_a \leq 10^5$,约 $5 \times 10^6$ 次操作。
- 动态规划:$O(\text{max}\_a \log \text{max}\_a)$,约 $1.7 \times 10^6$ 次操作。
- 总复杂度可接受。
#### C++ 代码实现
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
int max_a = 0;
long long sum = 0;
for (int i = 0; i < n; i++) {
cin >> a[i];
max_a = max(max_a, a[i]);
sum += a[i];
}
vector<long long> s(max_a + 1, 0);
for (int i = 1; i <= max_a; i++) {
for (int j = 0; j < n; j++) {
s[i] += a[j] / i;
}
}
vector<long long> dp(max_a + 1, LLONG_MAX);
dp[1] = sum;
for (int i = 1; i <= max_a; i++) {
if (dp[i] == LLONG_MAX) continue;
for (int k = 2; (long long)k * i <= max_a; k++) {
int j = k * i;
long long reduction = (k - 1) * s[j];
if (dp[i] - reduction < dp[j]) {
dp[j] = dp[i] - reduction;
}
}
}
long long ans = *min_element(dp.begin() + 1, dp.end());
cout << ans << endl;
return 0;
}
```
#### 样例测试
- **输入**:
`2`
`25 102`
- **输出**:
`4`
解释:设计面值 $\{1, 25, 100\}$,支付 25 用 1 个 25,支付 102 用 1 个 100 和 2 个 1,共 4 个硬币。
#### 相关拓展问题
1. **如何证明最优面值系统一定满足倍数关系?**
若存在非倍数关系面值,可通过调整成倍数关系减少总硬币数(数学归纳法证明)[^1]。
2. **当物品价格范围极大(如 $10^9$)时如何优化?**
- 预处理 $s[j]$ 时使用数论分块,将计算复杂度优化至 $O(n \sqrt{\text{max}\_a})$。
- 动态规划中只枚举有效的 $j$($s[j] > 0$ 时)。
3. **是否存在贪心或数学解法?**
本题需枚举所有倍数路径,贪心选择局部最优的 $k$ 可能错过全局最优解[^2]。
[^1]: BZOJ3233 [Ahoi2013]找硬币,样例解释与倍数关系性质。
[^2]: BZOJ刷题记录,动态规划在硬币系统设计中的应用。