火柴棍等式 ,但是数据范围有一点大……

news/2024/11/17 17:05:01/文章来源:https://www.cnblogs.com/Iictiw/p/18374443

题目描述

P1149

给你 \(n\) 根火柴棍,你可以拼出多少个形如 \(A+B=C\) 的等式?等式中的 \(A\)\(B\)\(C\) 是用火柴棍拼出的整数(若该数非零,则最高位不能是 \(0\))。用火柴棍拼数字 \(0\sim9\) 的拼法如图所示:

注意:

  1. 加号与等号各自需要两根火柴棍;
  2. 如果 \(A\neq B\),则 \(A+B=C\)\(B+A=C\) 视为不同的等式(\(A,B,C\geq0\));
  3. \(n\) 根火柴棍必须全部用上。

原题中 \(n \leq 24\),我们希望能做到更大。

当然,方案数(也许)会相当多,为了不写高精,我们可以将答案对某个质数取模(比如 \(10^9+7\))。


定个小目标,看看能不能做到 \(n \leq 100\)

由于 \(n\) 的范围增大,枚举一个数的代价相当高,原题中基于可能的 \(A,B,C\) 很小的做法不再适用,我们需要另辟蹊径。

尝试搜索,首先去除加号与等于号(将 \(n\) 减四),然后自低位向高位考虑 \(A,B\) 每一位的取值并计算是否合法。

想想我们需要知道什么:当前搜索到的深度(即从低到高的位数),上一位是否有进位,当前用了多少火柴棍。

容易写出简单的代码。

int cnt[]={6,2,5,5,4,5,6,3,7,6};
void dfs(int k,int carry,int sum){//k :当前考虑的位置,carry :上一位的进位,sum :还未用的火柴棍数量//calc the answerfor(int a=0;a<=9;a++)for(int b=0;b<=9;b++){//枚举A,B的第 k 位所填的数int tmp=a+b+carry;int c=tmp/10,new_carry=tmp/10;dfs(k+1,new_carry,sum-cnt[a]-cnt[b]-cnt[c]);}
}

但这个代码会有些问题:当 \(A\)\(B\) 中的某个数已经填完后,我们的算法会往前补前导 \(0\),这是题目所不允许的;同时,答案的统计也不方便。

由此,我们可以在填到某个位置后钦定 \(A\)\(B\) 被 “截断”, 即强制 \(A\)\(B\) 中的某个数以后只能选 \(0\),且不计入火柴棍总数,由此来尝试规避前导 \(0\) 对答案的影响。

每次填数后,考虑 \(A,B\) 是否被截断。仅需额外开两维记录。

这样一来,统计答案变得十分方便:当 \(A\)\(B\) 都被截断时进行统计,若此时无进位,则 \(sum = 0\) 时加入答案;若此时有进位,则 \(sum = 2\) 时加入答案。

int ans;
int cnt[]={6,2,5,5,4,5,6,3,7,6};
void dfs(int k,int carry,bool cuta,bool cutb,int sum){//cuta,cutb:a,b是否被截断if(cuta&&cutb){if((!carry&&sum==0)||(carry&&sum==2))ans++;return;}for(int a=0;a<=9;a++){if(cuta&&a!=0)break;for(int b=0;b<=9;b++){if(cutb&&b!=0)break;int tmp=a+b+carry;int c=tmp/10,new_carry=tmp/10;int new_sum=sum;if(!cuta)new_sum-=cnt[a];if(!cutb)new_sum-=cnt[b];new_sum-=cnt[c];if(new_sum<0)continue;if(!cuta&&!cutb){//枚举A,B是否截断dfs(k+1,new_carry,0,0,new_sum);dfs(k+1,new_carry,0,1,new_sum);dfs(k+1,new_carry,1,0,new_sum);dfs(k+1,new_carry,1,1,new_sum);}else if(!cuta&&cutb){dfs(k+1,new_carry,0,1,new_sum);dfs(k+1,new_carry,1,1,new_sum);}else if(cuta&&!cutb){dfs(k+1,new_carry,1,0,new_sum);dfs(k+1,new_carry,1,1,new_sum);}else dfs(k+1,new_carry,1,1,new_sum);}}
}

但是在原题会 WA 一个点。

而且效率相当低,\(n=50\) 左右就稳定的跑不过去。

还是因为前导 \(0\) 的影响,\(A\)\(B\)可以先选上一串前导 \(0\),然后再悠哉游哉地截断,我们的截断毫无作用。

可以注意到,如果当前位被填了 \(0\),那么当前位一定不能被截断(除非这个数就是 \(0\)),于是可以在截断时特判,只有在当前位非 \(0\) 或当前是第一位才允许截断。

int ans;
int cnt[]={6,2,5,5,4,5,6,3,7,6};
void dfs(int k,int carry,bool cuta,bool cutb,int sum){if(cuta&&cutb){if((!carry&&sum==0)||(carry&&sum==2))ans++;return;}for(int a=0;a<=9;a++){if(cuta&&a!=0)break;for(int b=0;b<=9;b++){if(cutb&&b!=0)break;int tmp=a+b+carry;int c=tmp%10,new_carry=tmp/10;int new_sum=sum;if(!cuta)new_sum-=cnt[a];if(!cutb)new_sum-=cnt[b];new_sum-=cnt[c];if(new_sum<0)continue;bool allowcuta=a!=0||k==1,allowcutb=b!=0||k==1;//只有当前位非0或在第一位才允许截断if(!cuta&&!cutb){dfs(k+1,new_carry,0,0,new_sum);if(allowcuta)dfs(k+1,new_carry,0,1,new_sum);if(allowcutb)dfs(k+1,new_carry,1,0,new_sum);if(allowcuta&&allowcutb)dfs(k+1,new_carry,1,1,new_sum);}else if(!cuta&&cutb){dfs(k+1,new_carry,0,1,new_sum);if(allowcuta)dfs(k+1,new_carry,1,1,new_sum);}else if(cuta&&!cutb){dfs(k+1,new_carry,1,0,new_sum);if(allowcutb)dfs(k+1,new_carry,1,1,new_sum);}else dfs(k+1,new_carry,1,1,new_sum);}}
}

现在可以通过原题了,但跑 \(n=50\) 还是要两三秒。

可以发现,目前我们的搜索向下递归的参数有且仅有函数的参数,没有任何额外信息(比如回溯需要的信息)。因此,可以使用记忆化搜索存储每个状态。

int cnt[]={6,2,5,5,4,5,6,3,7,6};
int f[N][2][2][2][N];//不难发现,每次进位最多为 1,所以 carry 这维仅需开到   2 
int dfs(int k,int carry,bool cuta,bool cutb,int sum){if(cuta&&cutb){if((!carry&&sum==0)||(carry&&sum==2))return 1;else return 0;}if(f[k][carry][cuta][cutb][sum]!=-1)return f[k][carry][cuta][cutb][sum];int ret=0;for(int a=0;a<=9;a++){if(cuta&&a!=0)break;for(int b=0;b<=9;b++){if(cutb&&b!=0)break;int tmp=a+b+carry;int c=tmp%10,new_carry=tmp/10;int new_sum=sum;if(!cuta)new_sum-=cnt[a];if(!cutb)new_sum-=cnt[b];new_sum-=cnt[c];if(new_sum<0)continue;bool allowcuta=a!=0||k==1,allowcutb=b!=0||k==1;if(!cuta&&!cutb){(ret+=dfs(k+1,new_carry,0,0,new_sum))%=mod;if(allowcuta)(ret+=dfs(k+1,new_carry,0,1,new_sum))%=mod;if(allowcutb)(ret+=dfs(k+1,new_carry,1,0,new_sum))%=mod;if(allowcuta&&allowcutb)(ret+=dfs(k+1,new_carry,1,1,new_sum))%=mod;}else if(!cuta&&cutb){(ret+=dfs(k+1,new_carry,0,1,new_sum))%=mod;if(allowcuta)(ret+=dfs(k+1,new_carry,1,1,new_sum))%=mod;}else if(cuta&&!cutb){(ret+=dfs(k+1,new_carry,1,0,new_sum))%=mod;if(allowcutb)(ret+=dfs(k+1,new_carry,1,1,new_sum))%=mod;}else (ret+=dfs(k+1,new_carry,1,1,new_sum))%=mod;}}return f[k][carry][cuta][cutb][sum]=ret;
}

程序效率得到了质的飞跃,\(n = 100\) 很快就跑过去了,\(n=1000\) 一秒内也冲过去了。

还能更进一步吗?比如 \(n=30000\) ?

乍一看有点难:总的状态数已经是 \(\Theta(n^2)\) 的了,怎么进一步优化?

仔细看:\(k\) 这一维似乎相当没用。\(k\) 唯一的用处就是判断前导 \(0\),没有任何其他用途。

事实上,我们仅需知道 \(k\) 是否等于 \(1\) 以判断前导 \(0\),因此可以直接将第一维压至 \(0/1\)

int cnt[]={6,2,5,5,4,5,6,3,7,6};
int f[2][2][2][2][N];
int dfs(bool k,bool carry,bool cuta,bool cutb,int sum){if(cuta&&cutb){if((!carry&&sum==0)||(carry&&sum==2))return 1;else return 0;}if(f[k][carry][cuta][cutb][sum]!=-1)return f[k][carry][cuta][cutb][sum];int ret=0;for(int a=0;a<=9;a++){if(cuta&&a!=0)break; for(int b=0;b<=9;b++){if(cutb&&b!=0)break;int tmp=a+b+carry;int c=tmp%10,new_carry=tmp/10;int new_sum=sum;if(!cuta)new_sum-=cnt[a];if(!cutb)new_sum-=cnt[b];new_sum-=cnt[c];if(new_sum<0)continue;bool allowcuta=a!=0||k==1,allowcutb=b!=0||k==1;if(!cuta&&!cutb){(ret+=dfs(0,new_carry,0,0,new_sum))%=mod;if(allowcuta)(ret+=dfs(0,new_carry,0,1,new_sum))%=mod;if(allowcutb)(ret+=dfs(0,new_carry,1,0,new_sum))%=mod;if(allowcuta&&allowcutb)(ret+=dfs(0,new_carry,1,1,new_sum))%=mod;}else if(!cuta&&cutb){(ret+=dfs(0,new_carry,0,1,new_sum))%=mod;if(allowcuta)(ret+=dfs(0,new_carry,1,1,new_sum))%=mod;}else if(cuta&&!cutb){(ret+=dfs(0,new_carry,1,0,new_sum))%=mod;if(allowcutb)(ret+=dfs(0,new_carry,1,1,new_sum))%=mod;}else (ret+=dfs(0,new_carry,1,1,new_sum))%=mod;}}return f[k][carry][cuta][cutb][sum]=ret;
}

现在程序效率大大提高(大常数的 \(\Theta(n)\)),一秒跑 \(30000\) 毫无压力,只是常数有点大,跑 \(10^5\) 可能要两三秒。

正文部分结束了,接下来是吹水。


要不要把数据范围再开大一点,比如说 \(n \leq 10^9\) ,甚至 \(10^{18}\)

首先,虽然比较,但原来的记搜是可以改写成一般的递推形式的。

很抽象,但是可以看出来,这确实是一个 1D/0D 的动态规划,只是常数有一点大而已(迫真)。

进一步地,每一位最坏的情况就是 \(A,B\)\(8\)\(C\)\(6\),只会令 \(sum\) 减去 \(20\)

换句话说,每个 \(sum\) 的转移只依赖于前 \(20\) 个状态,而且系数对于每个 \(sum\) 都是相同的。

有没有发现什么?

可以将 \(20 \times 2^4\) 个位置压在一起,用矩阵加速转移,理论复杂度是 \(320^3 \times \log n\) 左右。

稍微把时限放开点(或者机子比较快),跑 \(10^{24}\) 也许能冲一冲。

有点感慨,经过一系列优化,我们将原题 \(n=24\) 的数据范围一路加到了 \(n=10^{24}\)

不能说这是算法竞赛的全部,只能说不断优化算法,就是算法竞赛最大的乐趣之一吧。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/785507.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

manim边学边做--直线类

直线是最常用的二维结构,也是构造其他二维图形的基础。manim中针对线性结构提供了很多模块,本篇主要介绍常用的几个直线类的模块。Line:通用直线 DashedLine:各种类型的虚线 TangentLine:根据已有的几何体,绘制它的切线 LabeledLine:带有标签的直线其中,DashedLine,Ta…

AP5174内置PWM调节LED灯亮度输入5-100V车灯驱动IC 手电筒与车灯方案

产品描述 AP5174 是一款效率高,稳定可靠的 LED 灯恒流驱动控制芯片,内置高精度比较器,固定 关断时间控制电路,恒流驱动电路等,特别适合大功率 LED 恒流驱动。 AP5174 采用 ESOP8 封装,散热片内置接 SW 脚,通过调节外置电流检测的电阻值来设置 流过 LED 灯的电流,支持外…

Adobe Illustrator AI v28下载及安装教程 (矢量图形设计软件)

前言 Adobe Illustrator(简称AI)专业矢量图形设计软件,矢量绘图设计工具,设计师常用的矢量绘制软件。该软件广泛应用于广告设计、印刷出版、海报书籍、插画绘制、图像处理、PDF文档设计、WEB页面等设计,借助这款矢量绘图工具,可以制作适用于印刷,Web,视频和移动设备的徽标…

DPDK简介和原理

DPDK是一种绕过内核直接在用户态收发包来解决内核性能的瓶颈技术。本文分享自天翼云开发者社区《DPDK简介和原理》,作者:s****n DPDK是一种绕过内核直接在用户态收发包来解决内核性能的瓶颈技术。 什么是中断 了解DPDK之前,首先需要先了解什么是中断,其实中断就是电信号,中…

Webpack 核心流程

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。本文作者:霜序三个阶段 初始化阶段初始化参数:从配置文件、配置对象、shell 参数中读取,与默认的配置参数结合得出最后的参数。 创建编译器…

算法与数据结构——基本数据类型与编码

基本数据类型 基本数据类型是计算机CPU可以直接进行运算的类型,在算法中直接被使用,主要包括以下几种整数类型byte、short、int、long。 浮点数类型float、double,用于表示小数 字符类型char,用于表示各种语言的字母、标点符号甚至表情符号等。 布尔类型bool,用于表示“是…

中国电信公布2024年中期业绩!

2024年上半年,中国电信紧抓发展机遇,完整准确全面贯彻新发展理念,坚定履行建设网络强国和数字中国、维护网信安全责任,持续深入实施云改数转战略,加快推动数字信息基础设施智能化、绿色化演进升级,加大高质量产品和服务供给,加速战略新兴业务规模拓展,全面深化改革开放…

这是一款轻量存储黑科技!

近日,天翼云联合InfoQ举办了以“新存储,更轻量”为主题的线上技术分享会。天翼云存储产品线总监潘亚莲以“轻量级存储集群控制器HBlock的设计理念与场景实践”为主题,讲解了HBlock在产品理念、价值主张、应用场景等方面的内容。近日,天翼云联合InfoQ举办了以“新存储,更轻…

xlsx-js-style前端组装表格数据下载到本地

1.数据格式: 对象:key/valueconst data1 = {合同 主体信息:{\n "合同编号": "FWTsAEY-S1T-2023-01",\n "项目编号": "FTCG2023000265A",\n "项目名称": "福田区第二人民医院食堂服务采购项直",\n "总计…

Cookie,Session Filter,Listener详解

HTTP请求的无状态性HTTP的无状态性是其一个重要的特征,指的是HTTP协议本身并不保留客户端与服务器交互的历史信息,换而言之,即每次的HTTP请求都是独立的,服务器在处理每一个请求时都不会记住前一个请求的状态无状态的含义独立性:每次的HTTP请求都是独立的,不依赖于之前的请求,即…

探索风扇产品模型的3D可视化魅力

在这个科技日新月异的时代,每一个细微的创新都能为我们的生活带来前所未有的便捷与享受。今天,就让我们一起踏入一场视觉与科技的盛宴,探索风扇产品模型如何通过3D可视化技术,重新定义家居生活的舒适与美学。想象一下,在炎炎夏日,你无需亲临实体店,只需轻点鼠标或滑动指…

Tree组件的快速定位更新节点的状态,以及修改节点的数据属性等操作

当我们点击树节点的时候我们常常只能获得树的id,那么我么如何获快速定位到树节点的内容呢,除此之外,当树已经存在时,但是缺少我们想要的内容时,我们想在树节点上添加我们需要的额外的内容时该怎么办,那么就是用以下方法可以快速定位到我们需要的节点并可以快速添加内容当…