【BZOJ3238】【Ahoi2013】差异 后缀自动机

本文介绍了一种利用SAM(Suffix Automaton,后缀自动机)算法解决字符串中最长公共前缀(LCP)问题的方法。通过构建后缀自动机并结合DP算法,有效地计算出所有字符串后缀之间的最长公共前缀长度,并给出完整实现代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载请注明出处谢谢、、http://blog.youkuaiyun.com/vmurder/article/details/42721101

首先 秦神QY Orz 

题解:

这道题后缀数组过于鬼畜(wo’tai’ruo’bu’gan’xie)

所以写了简单好写易于理解不用分治不用RMQ的SAM大叔。


题解:

首先其实我们需要一个后缀树,然后两个后缀的lcp就是它们lca的len。

后缀树可以通过反序后缀自动机得到,这个很水。

然后len的性质就是后缀自动机的那个len(我写的‘deep’)。

后缀树上DP就水了,随便乱搞就可以过了。

那个size[x]*size[x-1]是C(size[x],2)的意思。


分析:一个节点(状态)的deep就是以他为结束的最长子串长度,当然就是那个lca了233。


代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 1001000
#define T 26
using namespace std;
struct KSD
{
	int v,next;
}e[N];
int head[N],cnt;
inline void add(int u,int v)
{
	cnt++;
	e[cnt].v=v;
	e[cnt].next=head[u];
	head[u]=cnt;
}
int size[N];

int pa[N],son[N][T],dep[N];
int tot=1,last=1;
inline int newnode(int _dep){dep[++tot]=_dep;return tot;}
inline void SAM(int alp)
{
	int p=newnode(dep[last]+1);
	size[p]=1;
	int u=last;
	while(u&&!son[u][alp])son[u][alp]=p,u=pa[u];
	if(!u)pa[p]=1;
	else {
		int v=son[u][alp];
		if(dep[v]==dep[u]+1)pa[p]=v;
		else {
			int nv=newnode(dep[u]+1);
			pa[nv]=pa[v],pa[v]=pa[p]=nv;
			memcpy(son[nv],son[v],sizeof son[nv]);
			while(u&&son[u][alp]==v)son[u][alp]=nv,u=pa[u];
		}
	}
	last=p;
}
long long ans;
void tree_dp(int x,int p)
{
	int i,v;
	for(i=head[x];i;i=e[i].next)
	{
		tree_dp(v=e[i].v,x);
		size[x]+=size[v];
	}
	dep[x]-=dep[p];
	ans-=(long long)size[x]*(size[x]-1)*dep[x];
}
char str[N];
int main()
{
//	freopen("test.in","r",stdin);
	int i,j,k;
	scanf("%s",str);
	int len=strlen(str);
	for(i=len-1;i>=0;i--)SAM(str[i]-'a');
	ans=(long long)(len-1)*len*(len+1)>>1;
	for(i=2;i<=tot;i++)add(pa[i],i);
	for(i=head[1];i;i=e[i].next)tree_dp(e[i].v,1);
	cout<<ans<<endl;
	return 0;
}


<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刷题记录,动态规划在硬币系统设计中的应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值