洛谷普及B3691 [语言月赛202212] 狠狠地切割(Easy Version)
题目:[语言月赛202212] 狠狠地切割(Easy Version)
题号:B3691
难度:普及一
末尾包含对二分法优化的详细解释
题目分析
最后一句应该是本题的考查关键,关于筛选算法的时间优化,
但从功能理论上,我找到了两种方法,但是由于时间的限制,
一种是标记法,第二种是替换法,最终还是要加入二分法来优化时间。
果然,时间永远无法忽视,“时间是世界上一切成就的土壤。时间给空想者痛苦,给创造者幸福”
所以,为了幸福着想,写程序记得优化时间喔。
引入思路和代码
关于切割,我有以下思路,如果切割的位置不是某段的端点,那么本次切割就会产生一个额外的一段,否则不然,所以切割的位置决定了本次切割是 “ 有效切割 ” 还是 “ 无效切割 ” 。
不妨设定一个结构体,结构体元素如下
struct stu{
long long num;
int k;
};
scanf("%lld %lld",&n,&m);
struct stu sn[n+2];
struct stu sm[m+2];
其中num就和正常的数组一样,存取当前的数值,然后k用于标记作用,
因为每次在当前位置切割,如果有效,那么该位置的前一个位置和后一个位置定会分别变成两个数列的端点,此时这三个位置就会变成无法产生有效切割的切割点。
例如:原数列 ( 1 2 3 4 5 6 7 8 ) ,其中无法产生有效切割的无效切割点只有两个端点1和8,
如果在3处切割,将会变成 (1 2) 3 (4 5 6 7 8) 两个段。然后就变成1 2 4 8 四个无效切割点。
而我们要做的就是把变成无效切割点的元素标记起来,这样下次对某位置切割就能判断是否有效切割。我们需要一下代码。
for(int u=1;u<=m;u++){
scanf("%lld",&sm[u].num);
sm[u].k=0;}
sn[1].k=1;
sn[n].k=1;
for(int y=1;y<=m;y++)
for(int x=1;x<=n;x++)
if(sm[y].num==sn[x].num&&sn[x].k!=1)
{
pn++;
sn[x-1].k=1;
sn[x].k=1;
sn[x+1].k=1;
}
该代码,首先两端是默认被标记上的无效切割点,然后每次切割就会将当前被切割的点,和相邻两边的点变成无效切割点(因为已经是新的段落的端点),这样的话,不是有效切割的切割操作我们完全可以忽略过去,只需要统计被有效切割的点即可。
该方案的源代码是
#include<stdio.h>
struct stu{
long long num;
int k;
};
int main()
{
long long n,m,pn=1;
scanf("%lld %lld",&n,&m);
struct stu sn[n+2];
struct stu sm[m+2];
for(int i=1;i<=n;i++){
scanf("%lld",&sn[i].num);
sn[i].k=0;}
for(int u=1;u<=m;u++){
scanf("%lld",&sm[u].num);
sm[u].k=0;}
sn[1].k=1;
sn[n].k=1;
for(int y=1;y<=m;y++)
for(int x=1;x<=n;x++)
if(sm[y].num==sn[x].num&&sn[x].k!=1)
{
pn++;
sn[x-1].k=1;
sn[x].k=1;
sn[x+1].k=1;
}
printf("%lld",pn);
return 0;}
该方案在时间复杂度上肯定满足不了该题,但是思路是个好思路,得到的答案也是正确的,起重工pn++,是因为每次有效的切割都会多产生一个新的段落,而原本就有一个主段落,所以pn=1;
第二种优化方案
看完第一个标记法,可以看出,被标记上的数已经没用了,遍历到相应的值也会跳过掉,那既然没用了,干脆不标记了,直接替换掉就行了。
我们来看一种新的方法,替换法。
我们每次切割掉的数,直接将其换成一个不会影响后续操作,而且又能分辨出的数值,比如我取的-1,例如原本的数列 1 2 3 4 5 6 7 8 9,如果切割其中的3 和7,8.
得到的新数列就是 1 2 -1 4 5 6 -1 -1 9;从中我们可以看出,就算切割的是无效端点,那么也不会影响到什么,因为-1叠加起来也不会影响到段落的分布。
最后我们只需要检测其中段落的个数,检测的方法也很简单,我们首先在数组的末端再插一个-1.
这样我们只需要检测a[i]!=-1&&a[i+1]==-1;满足这个条件说明检测到了一个小段的末尾。
所以代码如下。(同样由于时间问题,在我的方法的基础上,使用二分法进行优化,替代掉最后一步双层遍历的操作)
#include<stdio.h>
#include<stdlib.h>
int compare(const void *a, const void *b) {
return *(long long*)a - *(long long*)b;
}
int main()
{
long long n,m;
scanf("%lld %lld",&n,&m);
long long a[n+2];
long long b[m+2];
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
for(int i=1;i<=m;i++)
scanf("%lld",&b[i]);
a[n+1]=-1;
qsort(b+1, m, sizeof(long long), compare);
for(int x=1;x<=n;x++) {
long long *res = bsearch(&a[x], b+1, m, sizeof(long long), compare);
if(res) a[x] = -1;
}
long long p=0;
for(int i=1;i<=n;i++)
if(a[i]!=-1 && a[i+1]==-1)
p++;
printf("%lld",p);
return 0;
}
关于二分法,时间复杂度从本来的双层遍历O(n*m)变成O(n*logm),
其实二分法也是特别简单,不过是对半一致缩小范围,在这里多一条来解释一下二分法
二分法查找(Binary Search),也被叫做折半查找,是一种高效的查找算法,不过它要求被查找的数组必须是有序的(一般是升序)。该算法的核心思想是通过不断将查找区间一分为二,逐步缩小查找范围,直至找到目标元素或者确定目标元素不存在。下面为你详细介绍其原理、代码实现以及复杂度分析。
原理
- 初始化:设定两个指针,
left
指向数组的起始位置,right
指向数组的末尾位置。 - 查找过程:
- 计算中间位置
mid = (left + right) / 2
。 - 把中间位置的元素
arr[mid]
与目标元素target
进行比较:- 若
arr[mid]
等于target
,则查找成功,返回mid
。 - 若
arr[mid]
大于target
,表明目标元素在左半部分,更新right = mid - 1
。 - 若
arr[mid]
小于target
,表明目标元素在右半部分,更新left = mid + 1
。
- 若
- 计算中间位置
- 终止条件:
- 若找到目标元素,返回其索引。
- 若
left
大于right
,意味着查找区间为空,目标元素不存在,返回 -1。
代码举例
#include <stdio.h>
// 二分查找函数
int binarySearch(int arr[], int n, int target) {
int left = 0;
int right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid; // 找到目标元素,返回其索引
} else if (arr[mid] < target) {
left = mid + 1; // 目标元素在右半部分
} else {
right = mid - 1; // 目标元素在左半部分
}
}
return -1; // 未找到目标元素
}
int main() {
int arr[] = {1, 3, 5, 7, 9, 11, 13};
int n = sizeof(arr) / sizeof(arr[0]);
int target = 7;
int result = binarySearch(arr, n, target);
if (result != -1) {
printf("目标元素 %d 的索引是 %d\n", target, result);
} else {
printf("未找到目标元素 %d\n", target);
}
return 0;
}
代码解释
-
binarySearch
函数:- 该函数接收一个有序数组
arr
、数组长度n
以及目标元素target
作为参数。 - 运用
while
循环持续缩小查找区间,直至找到目标元素或者查找区间为空。 - 计算中间位置
mid
时,采用left + (right - left) / 2
来避免整数溢出。 - 若找到目标元素,返回其索引;若未找到,返回 -1。
- 该函数接收一个有序数组
-
main
函数:- 定义一个有序数组
arr
,并计算其长度n
。 - 设定目标元素
target
为 7。 - 调用
binarySearch
函数进行查找,并依据返回结果输出相应信息。
- 定义一个有序数组
复杂度分析
- 时间复杂度:每次查找都会将查找区间缩小一半,因此时间复杂度为 O(logn),这里的 n 是数组的长度。
- 空间复杂度:仅使用了常数级的额外空间,所以空间复杂度为 O(1)。
综上所述,二分法查找是一种高效的查找算法,不过前提是数组必须有序。在处理大规模有序数据时,它能显著提升查找效率。