[BZOJ3238][AHOI2013]差异(后缀数组)

本文深入探讨了字符串匹配算法中的求和式前两项计算方法,详细解析了如何利用单调栈统计LCP(i,j)中最小值的出现次数,通过具体代码展示了算法实现过程,包括构建SA数组、获取LCP数组及最终求解。

求和式的前两项可以直接算,问题是对于每对i,j计算LCP。

一个比较显然的性质是,LCP(i,j)是h[rk[i]+1~rk[j]]中的最小值。

从h的每个元素角度考虑,就是对每个h计算有多少对i,j以它为最小值。

在h中使用单调栈统计左右比它小的第一个元素,相乘得到结果。

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #define rep(i,l,r) for (int i=(l); i<=(r); i++)
 5 typedef long long ll;
 6 using namespace std;
 7 
 8 const int N=1000010,inf=1000000000;
 9 ll ans;
10 char s[N];
11 int n,top,x[N],y[N],c[N],sa[N],h[N],rk[N],l[N],r[N],stk[N];
12 int Cmp(int a,int b,int l){ return a+l<=n && b+l<=n && y[a]==y[b] && y[a+l]==y[b+l]; }
13 
14 void build_sa(int m){
15     memset(y,0,sizeof(y));
16     rep(i,0,m) c[i]=0;
17     rep(i,1,n) c[x[i]=s[i]-'a'+1]++;
18     rep(i,1,m) c[i]+=c[i-1];
19     for (int i=n; i; i--) sa[c[x[i]]--]=i;
20     for (int k=1,p=0; p<n; k<<=1,m=p){
21          p=0;
22          rep(i,n-k+1,n) y[++p]=i;
23          rep(i,1,n) if (sa[i]>k) y[++p]=sa[i]-k;
24          rep(i,0,m) c[i]=0;
25          rep(i,1,n) c[x[y[i]]]++;
26          rep(i,1,m) c[i]+=c[i-1];
27          for (int i=n; i; i--) sa[c[x[y[i]]]--]=y[i];
28          rep(i,1,n) y[i]=x[i]; p=1; x[sa[1]]=1;
29          rep(i,2,n) x[sa[i]]=Cmp(sa[i-1],sa[i],k) ? p : ++p;
30     }
31 }
32 
33 void get(){
34     int k=0;
35     rep(i,1,n) rk[sa[i]]=i;
36     rep(i,1,n){
37          for (int j=sa[rk[i]-1]; i+k<=n && j+k<=n && s[i+k]==s[j+k]; k++);
38          h[rk[i]]=k; if (k) k--;
39     }
40 }
41 
42 void solve(){
43     rep(i,1,n) ans+=1ll*i*(n-1);
44     h[0]=-inf;
45     rep(i,1,n){
46         while (h[i]<=h[stk[top]]) top--;
47         if (!stk[top]) l[i]=1; else l[i]=stk[top]+1;
48         stk[++top]=i;
49     }
50     h[n+1]=-inf; top=0; stk[0]=n+1;
51     for (int i=n; i; i--){
52         while (h[i]<h[stk[top]]) top--;
53         if (stk[top]==n+1) r[i]=n; else r[i]=stk[top]-1;
54         stk[++top]=i;
55     }
56     rep(i,1,n) ans-=2ll*(i-l[i]+1)*(r[i]-i+1)*h[i];
57 }
58 
59 int main(){
60     freopen("bzoj3238.in","r",stdin);
61     freopen("bzoj3238.out","w",stdout);
62     scanf("%s",s+1); n=strlen(s+1);
63     build_sa(300); get(); solve(); printf("%lld\n",ans);
64     return 0;
65 }

 

转载于:https://www.cnblogs.com/HocRiser/p/9441294.html

标题SpringBoot智能在线预约挂号系统研究AI更换标题第1章引言介绍智能在线预约挂号系统的研究背景、意义、国内外研究现状及论文创新点。1.1研究背景与意义阐述智能在线预约挂号系统对提升医疗服务效率的重要性。1.2国内外研究现状分析国内外智能在线预约挂号系统的研究与应用情况。1.3研究方法及创新点概述本文采用的技术路线、研究方法及主要创新点。第2章相关理论总结智能在线预约挂号系统相关理论,包括系统架构、开发技术等。2.1系统架构设计理论介绍系统架构设计的基本原则和常用方法。2.2SpringBoot开发框架理论阐述SpringBoot框架的特点、优势及其在系统开发中的应用。2.3数据库设计与管理理论介绍数据库设计原则、数据模型及数据库管理系统。2.4网络安全与数据保护理论讨论网络安全威胁、数据保护技术及其在系统中的应用。第3章SpringBoot智能在线预约挂号系统设计详细介绍系统的设计方案,包括功能模块划分、数据库设计等。3.1系统功能模块设计划分系统功能模块,如用户管理、挂号管理、医生排班等。3.2数据库设计与实现设计数据库表结构,确定字段类型、主键及外键关系。3.3用户界面设计设计用户友好的界面,提升用户体验。3.4系统安全设计阐述系统安全策略,包括用户认证、数据加密等。第4章系统实现与测试介绍系统的实现过程,包括编码、测试及优化等。4.1系统编码实现采用SpringBoot框架进行系统编码实现。4.2系统测试方法介绍系统测试的方法、步骤及测试用例设计。4.3系统性能测试与分析对系统进行性能测试,分析测试结果并提出优化建议。4.4系统优化与改进根据测试结果对系统进行优化和改进,提升系统性能。第5章研究结果呈现系统实现后的效果,包括功能实现、性能提升等。5.1系统功能实现效果展示系统各功能模块的实现效果,如挂号成功界面等。5.2系统性能提升效果对比优化前后的系统性能
在金融行业中,对信用风险的判断是核心环节之一,其结果对机构的信贷政策和风险控制策略有直接影响。本文将围绕如何借助机器学习方法,尤其是Sklearn工具包,建立用于判断信用状况的预测系统。文中将涵盖逻辑回归、支持向量机等常见方法,并通过实际操作流程进行说明。 一、机器学习基本概念 机器学习属于人工智能的子领域,其基本理念是通过数据自动学习规律,而非依赖人工设定规则。在信贷分析中,该技术可用于挖掘历史数据中的潜在规律,进而对未来的信用表现进行预测。 二、Sklearn工具包概述 Sklearn(Scikit-learn)是Python语言中广泛使用的机器学习模块,提供多种数据处理和建模功能。它简化了数据清洗、特征提取、模型构建、验证与优化等流程,是数据科学项目中的常用工具。 三、逻辑回归模型 逻辑回归是一种常用于分类任务的线性模型,特别适用于二类问题。在信用评估中,该模型可用于判断借款人是否可能违约。其通过逻辑函数将输出映射为0到1之间的概率值,从而表示违约的可能性。 四、支持向量机模型 支持向量机是一种用于监督学习的算法,适用于数据维度高、样本量小的情况。在信用分析中,该方法能够通过寻找最佳分割面,区分违约与非违约客户。通过选用不同核函数,可应对复杂的非线性关系,提升预测精度。 五、数据预处理步骤 在建模前,需对原始数据进行清理与转换,包括处理缺失值、识别异常点、标准化数值、筛选有效特征等。对于信用评分,常见的输入变量包括收入水平、负债比例、信用历史记录、职业稳定性等。预处理有助于减少噪声干扰,增强模型的适应性。 六、模型构建与验证 借助Sklearn,可以将数据集划分为训练集和测试集,并通过交叉验证调整参数以提升模型性能。常用评估指标包括准确率、召回率、F1值以及AUC-ROC曲线。在处理不平衡数据时,更应关注模型的召回率与特异性。 七、集成学习方法 为提升模型预测能力,可采用集成策略,如结合多个模型的预测结果。这有助于降低单一模型的偏差与方差,增强整体预测的稳定性与准确性。 综上,基于机器学习的信用评估系统可通过Sklearn中的多种算法,结合合理的数据处理与模型优化,实现对借款人信用状况的精准判断。在实际应用中,需持续调整模型以适应市场变化,保障预测结果的长期有效性。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
<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、付费专栏及课程。

余额充值