初级算法技巧 4

1.数学模型转换

理科中很多问题会给你一段场景描述,这个场景可能直接来源生活、或者来源于某个故事,然后会提出一个问题让你解决。

例如:一个圆形蛋糕被分成 16 等份后,小明想要吃其中的 A 块,小华想要吃其中的 B 块,但是同一个人不能拿相邻的两块蛋糕。请问他们能否拿到自己想要的数量的蛋糕?

类似这种题目我们要做的第一件事情是理解题干中描述的数学问题:一个蛋糕分成 16 份,想要蛋糕不相邻,换个角度思考,不能拿超过 8 个,超过了就肯定要相邻了;一个人拿完以后,剩下那个人只剩下 8 个不相邻的可以拿,多了也没有。

这题就转换成小明和小华每人各拿不超过 8 块就行,即 A<=8 and B<=8 即可。

当然这是一个比较简单的例子。在更多场合我们需要灵活使用计算、几何、二进制、坐标系等你熟悉的数学知识,将遇到的问题与你了解的数学模型关联起来-----一个问题可能可以关联不止一个数学模型,选择合适的数学模型,能够为你解题提供最佳途径。

2.纸币组成

现有面值为4元和7元的纸币各100张。请问用这些纸币能否组成N元(N<=100)

本题数学模型转换是不定方程问题(未知数的数量超过方程的数量),即 4x+7y=n 求整数解(x、y 可以为 0)。

从效能最优角度来思考,这里可以考虑只枚举 y,只有 n-y能被 4 整除,使得 x 可以取到整数解。注意 y 可以取 0,也就是不用 7 元纸币的意思。就不用二重循环枚举了。

为了简便这里直接上 7,就不再 1*7 了

cpp 复制代码
for(int y=0;y<=n;y+=7) 
{
    if((n-y)%4==0)
    {
        //有整数解
    }
}

3.给定一个正整数 n,请你计算在 1 和 n 之间有多少个奇数有 8 个因数(n<=200)

这个题目的方法有多种,当 n 不大时,这些方法都能行得通。

方法 1:如果1 到之间有正好 4 个数可以被 n 整除,且不是整数,那么这个 n 刚好满足 8 个因数的条件

解读:首先如果是整数,说存在整数 k=,满足 k*k=n,这样 n 的因数一定是奇数个(那就不是 8 了);其次既然左侧(比小)有 4 个因数,说明与这 4 个因素存在一一对应的 4 个更大的因数在右侧(因数都是一对对的出现)

方法 2:对 n 进行质因数分解。

8=4*2=2*2*2

也就是说这个 n 可以是 或者的形式 (质因数的分解在往期的初级算法技巧有总结)

【更复杂的思考】:如果 n 很大,最大可以到,应该如何处理这个问题?

首先方法 1 是必然超时的:其时间复杂度是*,已经超过了的安全上限,这个做法必然拿不到全分。

方法 2 需要进行一些调整:如果对于每个数 i 我们都是用"现做"的方式去分解质因数----对每个数的因数都是从 2 到 开始尝试进行分解,速度会要比方法 1 快一些(因为每找到一个)n 就要除掉这个因素,自身会缩小。

但是仍然有极端情况:如果某个 i 是个质数,且其大小接近,对这个 i 就可能超时!导致程序本身超时。

所以这里有一种做法,但是需要我们学完数组以后才能做:

既然 n 最大,首先枚举以内的质数放在数组里,然后在质因数分级的时候只要尝试这些质数就可以----这样可以大幅降低计算次数,使得在 n 很大的时候都不会超时。

总结:

一个题目可能有多个解法,但是在某些条件限制以后(如数据上限),这会导致一些原本可行的方法超时,需要谨慎的选择更合适的算法。

4.画空心菱形

从示例可以看出,当 n=3 时,输出了一个 5 层的菱形,这里讲两种绘制方法:

【使用模拟绘制】:

模拟法是一种通用的技法,如果我们没有办法总结归纳更好的办法时,模拟法是一种"寻求问题自然解"的做法:通过直观的描述这个事情怎么做,从而求解。

这种做没有算法上的难度,但是本题逻辑比较复杂,也是大家第一次接触到这种不难但是过程长、代码长的题目。

此题型主要考察对问题的理解是否全面、考虑是否周到、能否构建完全匹配问题的思维模式,不能遗漏。

这个菱形一共有 2n-1 层,并且以第 n 层为分界,从 1 到第 n 层逐渐变宽、从第 n 层到第 2n-层逐渐收拢

对于每一层来说共有4 个操作动作:

1.前空格

2.画*

3.中间空格

4.画*

其中第 1 层和第 2n-1 层例外,只有步骤 1、2,没有 3、4

观察前空格和中空格的数量:

以 n=3 为例

每层前空格的数量为:2 1 0 1 2

每层中间空格的数量为: 0 1 3 1 0

如果扩展到 n=4:

每层前空格的数量为:3 2 1 0 1 2 3

每层中间空格的数量为: 0 1 3 5 3 1 0

推理公式:其中 i 为行数(虽然题目只给了一个例子,但是我们需要扩展多个例子总结规律)

前空格: n-1 n-2 .... 0 1 2...n-2 n-1 ,让 i 从 1 开始,每一行输出的空格数量就是 n-i 。到第 n 行正好输出 0 个前空格

中空格: 0 1 3 .. 2(n-1)-1 2(n-1)-3 ... 3 1 0 ,让 i 从 1 开始, 每一行输出的中空格数量就是 2(n-i)-1, 注意 i 等于 1 时,这个值为-1,可以理解为不输出中空格


因为 1-n 是增长,n 到 2n-1 是下降,为了方便理解,这里可以把菱形拆成上下两部分输出,也就是由两个循环来处理。

大致的代码:

上半段的循环:

处理前空格的循环

输出*

处理中空格的循环。注意特判一层,第一层没有中空格和输出,在这一层要 continue

输出*

下半段的循环:

处理前空格的循环

输出*

处理中空格的循环 注意特判第 2n-1 层,没有中空格和输出

输出*

【捷径】

一般来说做起来比较复杂的题目都可能存在捷径。捷径需要你充分观察和理解题目,重绘数学模型,从而在高一个维度推演出来更简便的思路。

是的,所谓捷径,通常都是在高一个认知维度的降维打击。

站在盘外思考:

对于输入 n 来说,我们实际上绘制了了 (2n-1)*(2n-1)个格子,每个格子不是*就是空格

菱形是一个轴对称图形,这个题目给出的菱形是左右对称的,其中轴线正好是第 n 列,其菱形的中心点就是(n,n)。

当 n=5 时,如下图,C 是中心点,正好在(5,5)的位置。

|----|----|----|----|----|----|----|----|----|
| | | | | * | | | | |
| | | | * | | * | | | |
| | | * | | | | * | | |
| | * | | | | | | * | |
| * | | | | C | | | | * |
| | * | | | | | | * | |
| | | * | | | | * | | |
| | | | * | | * | | | |
| | | | | * | | | | |

对于每一个*,注意*的横坐标和纵坐标(左上角从 1,1 开始), 这个点对于中心点 (n,n)的横坐标和纵坐标的差值正好是 n-1,即dx-n 的绝对值和 dy-n 的绝对值之和正好就是 n-1。(观察总结)

满足这个条件就输出*,其他时候就输出空格。

如此一来这个问题简化很多了:

cpp 复制代码
int n;
cin>>n;

for(int x=1;x<=2*n-1;x++)
{
    for(int y=1;y<=2*n-1;y++)
    {
        if(abs(x-n)+abs(y-n)==n-1)
            cout<<"*";
        else
            cout<<" ";
    }
}

总结:观察角度的不同,决定了这个题目的复杂度。

模拟法可以帮你兜底,但不一定是最好的做法。更好的做法要看你对题目的理解程度和动手程度,信息学竞赛要求我们善于观察和发现规律,在学习信息学竞赛的过程中这两种能力也会得到充分锻炼。

5.二进制转十进制

我们目前的进度讲完了二进制但是还没有讲到数组和 string,且已经知晓 n 最多有 30 位,所以用 longlong 都是没有办法完整的接收 n 的。所以要另想办法。

对于一个二进制数例如:10101,其转换为十进制的规则为:

我们事先已经从题目知道了这个二进制有 n 位,这就方便我们处理了:这里可以在循环中用 char 读取二进制的每一位,比对当前的值,乘以二进制的权即可。

cpp 复制代码
int a;
char b;
cin>>a;
	
int q= 1<<(a-1); //通过位移预算直接得到最高位的权
	
int sum=0;
	
for(int i=0;i<a;i++)
{
	cin>>b;
		
	if(b=='1')
		sum+= q;
			
	q>>=1;//位权降低 1,效果等同于 /2
}
	
cout<<sum;

总结:

办法总比困难多,灵活运用现有的知识来帮助你解决问题。

6.四舍五入是一种常见的近似计算方法。现在,给定 n 个整数,你需要将每个整数四舍五入到最接近的整十数。例如,43 四舍五入后为 40,58 四舍五入后为 60。

C++中当然是没有现成的可以将整数四舍五入的方法的。

所以本题我们看思考这个四舍五入的本质:

当最后一位大于等于 5,那么十位就要加 1,否则十位不加,然后最后一位需要抹除。

那么我们要做的事情:

1.用%拿到个位a

2.将输入的数据 /10 再 *10,这样通过整除的特性抹掉了最后一位

3.判断a 的大小,如果大于等于 5,则第二步得到的数据+10

输出结果。

总结:当要做的事情无法一步到达时,拆解步骤,看看每一步是否都可以实现,那就整件事可以实现。

7."考虑无序对""不计顺序的有序对"

本题的数学模型实际上是直角三角形面积的整数解,这就要求两条边的至少有一个为偶数(这样才能确保a*b的结果可以被2整除,使得结果为整数),解决这个问题依靠枚举即可,a和b都是从1枚举到n,然后判断a和b是否至少有一个能整除2即可。

但是本题有一个附件条件:那就是当a和b调换以后,认为这是一个重复的三角形(不能重复计入次数)

那么这一题就要在a和b至少有一个偶数的基础上,只能计算"有序对"的ab(即如出现先a后b的情况,则不取先b后a)

本题其实有一个非常好的参考样例:99乘法表

以3*4举例,这个表是看不到4*3的

仔细观察乘法表,这个表都是 x*y=c的形式,我们可以看到这里的y都不小于x,即y>=x

应用同样的思路,我们可以来处理这个问题:

cpp 复制代码
int cnt=0;
for(int a=1;a<=n; a++)
{
    for(int b=a;b<=n;b++) 
    //关键点:b从a开始,从而确保b>=a
    {
        if(a%2==0 || b%2==0)
            cnt++;
    }
}

我们让第二轮循环b从第一轮循环的当前值开始,这样就完美的避开b<a的情形,所有符合条件的ab对都满足a<=b,确保ab对不会重复。

总结:

这种循环控制的方式是常见过滤无序对的技巧,在要求多个变量保持从小到大的关系是,也可以参考这个思路。例如:a、b、c三个数的和不超过n,要求a<b<c,求a、b、c的正整数解的数量,就可以使用这个思路。

8.幂和数

直观做法:如果直接使用2的n次方去暴力枚举,在n很大的时候必然会超时。

本题如果要使用更高效的解法,首先要明确对n的认知。

既然n可以表示为2个由2的若干次方的和,那么的n的二进制一定能表达为以下形式:

000000000000000000000000000110000

上述2进制数一定只有不超过2个1

解读:

题目没有明确x和y是否相同,这里分情况讨论:

1.当x和y相同时,原式可以表达为,很明显,此时n的二进制只有1个1

2.当x和y不同时,各占据一个1,此时n的二进制有2个1

所以本题的实质就是检测l和r之间所有数,看看这些数的二进制中有多少个数含有1的个数不超过2

cpp 复制代码
int cnt=0;
for(int i=l;i<=r;i++)
{
    int t=i;//一定要暂存一个t用来计算,因为i是不能变的
    int cnt2=0;
    while(t)
    {    
        if(t%2==1) // 可以替换为 t&1==1 ,其意义为通过与运算判断最低位是否为1,和当前等效
            cnt2++;

        t>>1;//右位移,等效 t/=2,但是速度更快
    }

    if(cnt2<=2)
        cnt++;

}

总结:

一题虽然可能多解,但是必然有1种思路是最契合题目设计意图的,这个思路通常来说是最优解。

相关推荐
xqqxqxxq2 小时前
洛谷算法1-1 模拟与高精度(NOIP经典真题解析)java(持续更新)
java·开发语言·算法
砍树+c+v2 小时前
3a 感知机训练过程示例(手算拆解,代码实现)
人工智能·算法·机器学习
zy_destiny2 小时前
【工业场景】用YOLOv26实现4种输电线隐患检测
人工智能·深度学习·算法·yolo·机器学习·计算机视觉·输电线隐患识别
智驱力人工智能2 小时前
货车违规变道检测 高速公路安全治理的工程实践 货车变道检测 高速公路货车违规变道抓拍系统 城市快速路货车压实线识别方案
人工智能·opencv·算法·安全·yolo·目标检测·边缘计算
罗湖老棍子2 小时前
【例9.18】合并石子(信息学奥赛一本通- P1274)从暴搜到区间 DP:石子合并的四种写法
算法·动态规划·区间dp·区间动态规划
2301_810730102 小时前
python第四次作业
数据结构·python·算法
adam_life2 小时前
区间动态# P1880 [NOI1995] 石子合并】
算法
坠金2 小时前
递归、递归和回溯的区别
算法
恋爱绝缘体12 小时前
Java语言提供了八种基本类型。六种数字类型【函数基数噶】
java·python·算法