[算法课]全面翻新计划!第六周全解

上课内容

分治法:【先分再治,先治再分】

例1 归并排序

【第4个排序,记得一共是6个】

思路:
1 将所有的数据一分为二(分成2部分)
注意: 如果是偶数个数值,刚好能均分为2部分
如果是奇数个数值,只能分成一边多一边少
【定:左多右少?左少右多?】
2 继续分,把左边的数据再分成2部分;右边的数据也再分成2部分。
3 层层往下分,分到最后就剩下每次只有1个数值
4 归:每次2组数值合并为1数组,并且合并后的数值是从小到大的(有序的)

在这里插入图片描述

实例说明
1 求中间位置的求法:mid=(left+right)/2
2 定:左多右少。2个部分的各自范围是[left,mid]和[mid+1,right]
3 数据的合并:数据合并过程是不能在数据的原始存储位置上操作。
建议:将合并为有序的数值另外存放在一段存储空间中【有序的数值】
然后再把它们存入原本的存储位置
4 举例
在这里插入图片描述
提醒: 整个该过程2区域的值合并的过程中,要进行比较
如果谁小则把谁放在新的存储空间中。
继续比较。
但是会存在一种很特别的情况。
可能某一边的所有数值都已经存入新的存储空间中了。
而另外一边还有部分剩下的数值却还没有存入新的存储空间中。
将剩下的所有的数值直接存入新的存储空间中。
举例:刚才6存入新存储空间中,【且右边没有数值了】
但是左边的7和9还没有存入新存储空间中。所以直接全部依次放入就好了。

颜老板版本

#include<stdio.h>

double a[1000];//全局数组

void hebing(int left,int mid,int right)//这3个参数会涉及到2个部分的区间范围
{
	//合并过程建议:使用另外一段新的存储空间来存储临时的有序数据
	double *t;
	int i,j,m,k;
	
	t=(double *)malloc( (right-left+1)*8 );
	//t指向的是这段存储空间
	//在左边和右边2块数据区域中合并成1块且有序,整个过程是要比较大小的。
	//所以准备i变量从左边区域和j变量从右边区域开始分别取值
	i=left;	//左边区域的第1个数值 
	j=mid+1;//右边区域的第1个数值 
	k=0;	//t所指向的存储空间的首个位置,是从0开始编号
	
	while(i<=mid && j<=right)//只要左边区域和右边区域的值没有比较完就继续循环 
	{
		if(a[i]>a[j])//左边取的数值 大于 右边取的数值,则放右边的数值到t所指向的存储空间中 
		{
			t[k]=a[j];k++;j++;
		}	
		else
		{
			t[k]=a[i];k++;i++;
		}
	} 
	//提醒:上述操作将数组按大小顺序放入临时的存储空间中
	//但是会导致某一边还有剩下的数值没有存入临时空间中
	//不确定左边区域有剩下,还是右边区域有剩下
	//既然不确定2个区域都讨论
	while(i<=mid)
	{
		t[k]=a[i];k++;i++;	
	}
	while(j<=right)
	{
		t[k]=a[j];k++;j++;	
	}
	//最后将t存储空间中从位置0到位置right-left的数值直接赋值到a数组中的【left,right】中
	//a数组中的存储空间是从left开始,依次+1 
	for(m=0;m<=right-left;m++)
		a[left+m]=t[m]; 
}  

//归并排序算法自定义函数
void mergesort(int left,int right)
{
	int mid;
	mid=(left+right)/2;
	
	//满足不止1个数值的时候,需要将数据部分分成2部分。只有1个数值的时候一定是left等于right
	if(left<right) //如果该条件为真,则至少2个,可以分 
	{
		mergesort(left,mid);//左边区域 
		mergesort(mid+1,right);//右边区域 
		hebing(left,mid,right);//调用合并函数 
	}
	else
		return ;	
}

int main()
{
	int i,n;
	
	printf("输入数据个数:");
	scanf("%d",&n);
	
	printf("输入%d个数值\n",n);
	for(i=0;i<n;i++)
		scanf("%lf",&a[i]);
		
	//调用归并函数
	mergesort(0,n-1);
	
	printf("输出排序后的数值【从小到大】\n",n);
	for(i=0;i<n;i++)
		printf("%lf\n",a[i]);	 
	return 0;
}

更新版

#include<iostream>

using namespace std;

const int N=1e5;
int a[N],t[N];

void mergeSort(int l,int r)
{
    if(l==r)return ;
    int mid = l+r>>1;
    
    mergeSort(l,mid);
    mergeSort(mid+1,r);
    
    int i=l,j=mid+1;
    
    int idx=0;
    while(i<=mid&&j<=r)
        if(a[i]<=a[j])t[idx++]=a[i++];
        else t[idx++]=a[j++];
        
    while(i<=mid)t[idx++]=a[i++];
    while(j<=r)t[idx++]=a[j++];
    
    for(int i=0,j=l;j<=r;j++,i++)a[j]=t[i];
}
int main()
{
    int n;
    cin>>n;
    
    for(int i=0;i<n;i++)cin>>a[i];
    
    mergeSort(0,n-1);
    
    for(int i=0;i<n;i++)cout<<a[i]<<" ";
    return 0;
}

了解更多

如果你想了解更多的[归并排序的逻辑,可以直接参照我写的归并排序,或者在B站查询更多优质UP主的视频,而视频方式的内容或许能让你理解得更快
如果你想了解颜老师要求的六种排序算法,你可以查看我的六种排序

例2 折半查找

查找分类
(1)顺序查找
(2)折半查找

(1)顺序查找
思路:将所要查询的数据和已经存在的所有数据进行依次比对,如果相同则找到

颜老板版本

#include<stdio.h>

double a[1000];//全局数组

void search(double x,int n)//要已知查询对象和数据的总个数
{
	int i,flag=0;//表示一开始没有找到
	for(i=0;i<n;i++)
	{
		if(x==a[i])
		{
			printf("第%d个数值%lf是所要查找的对象\n",i+1,x);
			flag=1;	
		}	
	}	
	
	if(flag==0)
		printf("查无此对象\n");
}

int main()
{
	int i,n;
	double x; 
	
	printf("输入数据个数:");
	scanf("%d",&n);
	
	printf("输入%d个数值\n",n);
	for(i=0;i<n;i++)
		scanf("%lf",&a[i]);
		
	printf("输入查找对象:");
	scanf("%lf",&x);
		
	//调用查询函数 
	search(x,n);	
	 
	return 0;
}

要知道:顺序查找是简单,但是如果数据多了,麻烦了。比如想象数据达到百万条及以上。
顺序查找的效率是低的。
注意:可以查重复值。

注明

没有看和学的必要,因为都会

折半查找逻辑

(2)折半查找
提前:必须是对有序(升序还是降序)的数据进行查询。否则不能进行折半查找!【灵魂所在】

思路:
对有序的数值,求中间值,判断这个中间是否是所要查询的对象,如果是则输出,
如果不是所要查找的对象
则立即比较大小
如果中间值小于查找值,则继续再右边进行查找
如果中间值大于查找值,则继续再左边进行查找
一直重复下去。【明显能体现出 分治法的思路】
举例说明

假设找8									
				a[0]	a[1]	a[2]	a[3]	a[4]	a[5]	a[6]	a[7]	a[8]
原始数据			1		2		3		4		5		6		7		8		9
第1次折半查找		1		2		3		4		5		6		7		8		9
第2次折半查找												6	    7	    8	    9
第2次折半查找								                                8		9
									找到了	
									一共进行3次

颜老板版本

#include<stdio.h>

double a[1000];//全局数组 

void sort(int n)//书写一个排序函数 
{
	int i,j;
	double t;
	for(i=0;i<n-1;i++)
	for(j=0;j<n-1;j++)
	{
		if(a[j]>a[j+1])
		{
			t=a[j];
			a[j]=a[j+1];
			a[j+1]=t;
		}
	}
}

void midsearch(double x,int left,int right)
{
	int mid=(left+right)/2;
	if(a[mid]==x)
		printf("第%d个数值%lf是所要查找的值\n",mid+1,x);
	else
	{
		if(a[mid]<x)//所以应该在mid的右边继续找
			midsearch(x,mid+1,right);
		else //所以应该在mid的左边继续找
			midsearch(x,left,mid-1); 
	}
}

int main()
{
	int i,n;
	double x; 
	
	printf("输入数据个数:");
	scanf("%d",&n);
	
	printf("输入%d个数值\n",n);
	for(i=0;i<n;i++)
		scanf("%lf",&a[i]);
		
	printf("输入查找对象:");
	scanf("%lf",&x);
		
	//调用查询函数 
	midsearch(x,0,n-1);	
	 
	return 0;
}

更新版

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1e5;
int a[N],t[N];

int binnarySearch(int l,int r,int x)
{
    int mid = l+r>>1;
    int midNum = a[mid];
    
    if(midNum == x)return mid+1;
    
    if(x>midNum)binnarySearch(mid+1,r,x);
    else binnarySearch(l,mid-1,x);
}
int main()
{
    int n,x;
    cin>>n>>x;
    
    for(int i=0;i<n;i++)cin>>a[i];
    
    sort(a,a+n);
    
    cout<<binnarySearch(0,n-1,x);
    
    return 0;
}

测试数据

500 277
    218 97 78 13 7 259 14 90 86 460 72 134 458 2 245 490 126 33 355 171 166 433 16 74 436 167 5 296 350 148 146 279 230 467
    182 31 315 77 151 442 253 224 156 325 379 183 30 47 129 124 441 497 314 193 390 463 80 362 461 280 383 185 415 179 174
    234 214 1 199 244 249 398 473 136 394 349 369 63 83 356 440 363 219 484 330 425 465 176 125 269 289 312 162 451 480
    100 342 418 243 177 149 10 422 42 478 338 141 367 346 271 221 283 114 443 216 145 235 220 380 204 147 24 307 285 403
    175 256 477 305 165 416 420 255 227 211 427 370 223 195 9 69 264 35 17 38 384 56 329 300 246 178 203 39 471 55 46 
    103 263 455 231 487 445 188 268 482 58 294 257 449 391 500 392 479 261 492 96 267 88 116 494 206 43 262 71 180 120 
    142 351 29 340 130 310 57 431 226 276 6 437 187 447 270 345 66 374 210 133 348 215 242 419 469 435 37 331 84 59 28 
    117 430 426 488 222 399 452 448 273 158 150 170 40 110 217 229 232 76 303 382 241 275 108 366 409 499 26 311 302 
    486 91 347 334 94 358 491 196 107 360 401 292 191 4 297 475 378 298 236 389 22 359 233 163 212 92 53 288 472 476 
    327 153 278 99 483 115 143 407 205 344 498 316 132 8 190 181 113 160 317 410 48 60 105 127 239 250 400 70 213 377 
    154 474 319 322 495 44 397 62 313 411 393 164 496 111 51 291 429 172 470 168 54 157 225 208 20 328 417 106 324 135 
    489 240 326 64 265 404 450 318 98 444 237 308 104 352 155 438 396 152 293 25 371 68 408 432 112 301 73 11 128 354 
    251 254 87 434 247 34 332 252 192 258 323 412 341 12 385 45 65 228 357 466 281 49 194 284 137 406 372 485 454 123 
    36 169 209 41 140 353 299 375 402 320 121 459 457 138 75 421 144 274 339 93 207 197 82 61 200 287 364 102 266 50 
    260 343 373 81 456 198 202 173 388 85 282 23 290 386 333 446 424 189 462 95 468 286 335 361 295 321 368 186 413 101
    67 131 89 79 439 481 122 118 387 139 27 18 395 52 423 405 381 336 184 414 3 428 119 309 493 453 161 306 15 464 248 
    109 21 365 376 32 19 272 201 238 159 337 304 277

折半思考题

注意:上述折半查找的算法是查找出的值是唯一的。
但是大多数据中体现出的是数据非唯一。
下来思考并修改该程序,改为可以查找非唯一性数值。

代码

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1e5;
int a[N],findArr[N];
int idx =0;

void binnarySearch(int l,int r,int x)
{
    
    int mid = l+r>>1;
    int midNum = a[mid];
    
    //cout<<l<<" "<<mid<<" "<<midNum<<" "<<r<<endl; 
    if(midNum == x)findArr[idx++]=mid+1;
     
    if(l==r)return ;
    
    if(x>=midNum)binnarySearch(mid+1,r,x);
    if(x<=midNum)binnarySearch(l,mid-1,x);
}
int main()
{
    int n,x;
    cin>>n>>x;
    
    for(int i=0;i<n;i++)cin>>a[i];
    
    sort(a,a+n);
    
    binnarySearch(0,n-1,x);
    
    for(int i=0;i<idx;i++)cout<<findArr[i]<<" ";
    
    return 0;
}

测试数据

			1 :3 2 2 2 2
			2 : 6 2 1 2 2 2 3 3
			8 2 1 2 2 2 2 4 5 6

注意

强制修改了
结束条件,
分治条件

因为存在N个相同的情况,所以需要两边一起判断,同时结束条件也可能有问题,

! 最大测试数据只通过到了8

例3 最重要的程序

大家是否知道在2019年国庆阅兵典礼中最后一个出场的是谁?
中国女排!
在2019年9月30日那天,获得了女排世界杯冠军!!!

是11连胜夺冠!!!

女排世界杯的赛制
在这里插入图片描述

比赛赛制采用12支队伍单循环,
两两捉对厮杀一场定胜负,依次进行。
比赛开始把12个队分成AB两个组进行,
当同组每一个选手都碰面以后,

再重新分组进行比赛,
确保12支队伍每两支都相遇一次,
最终将根据先胜负场次后积分的排名顺序。

今天我们所要编写的程序就是输入球队的数量,得出比赛的对阵表!【分组处理】

为了方便起见,借用百度中对象 对球名称和对应的编号【编号是种子队编号】
提醒:
1 题目给与12个队伍,最终是考虑任意个队伍
2 如果12个队伍,比赛一共是进行了11天
3如果12个队伍,每天的比赛场数应该是6场

分析

1 先分析2个队伍的时候,比赛对阵表,如何表示?

在这里插入图片描述

以4个队伍的比赛赛程安排是建立在黄色区域为已知基础上的!
蓝色区域:对应的位置是黄色区域的值+2
绿色区域:对应的位置是黄色区域的值+2
红色区域:对应的位置是黄色区域的值一样大

4 5,6,7只队伍暂时不考虑。直接考虑8个队伍!
在这里插入图片描述

是分治法: 把8个队伍的数据表示分成4个区域。
先搞定左上角区域。
而左上角区域要搞定,又要借用4个队伍的比赛赛程安排方法。
而4个队伍的数据表示又要分成4个区域。
。。。。。。。。。。

首先准备数组的初始值为
a[0][0]=1; a[0][1]=2;
a[1][0]=2; a[1][1]=1;


第1轮:分析4个队伍的【相当于八个队伍的左上角】
//左下角,蓝色区域
for(i=0;i<=1;i++)
for(j=0;j<=1;j++)
	a[i+2][j]=a[i][j]+2;

//右上角,绿色区域
for(i=0;i<=1;i++)
for(j=0;j<=1;j++)
	a[i][j+2]=a[i][j]+2;

//右下角,红色区域
for(i=0;i<=1;i++)
for(j=0;j<=1;j++)
	a[i+2][j+2]=a[i][j];

第2轮:分析8个队伍的
//左下角,蓝色区域
for(i=0;i<=3;i++)
for(j=0;j<=3;j++)
	a[i+4][j]=a[i][j]+4;

//右上角,绿色区域
for(i=0;i<=3;i++)
for(j=0;j<=3;j++)
	a[i][j+4]=a[i][j]+4;

//右下角,红色区域
for(i=0;i<=3;i++)
for(j=0;j<=3;j++)
	a[i+4][j+4]=a[i][j];

8个队伍,一共进行了2轮
4个队伍,一共进行了1轮
2个队伍,一共进行了0轮【因为那个1221的组合是已知啊!】
2^(n+1)个队伍,一共进行了n轮【最后我选它来写程序】
反之也可以
n个队伍,一共进行了?轮 【可以使用这个来做】

提醒:5,6,7的如何处理,直接去掉8个队伍中多余的就行了

颜老板代码

#include<stdio.h>
#include<math.h>

int a[50][50];

void f(int n)
{
	int i,j,k;
	//这是2个队伍的比赛的赛程,我们作为初始值 
	a[0][0]=1;	a[0][1]=2;
	a[1][0]=2;	a[1][1]=1;
	
	for(k=1;k<=n;k++)//n=0的时候是0轮,n=1的时候是1轮,n=2的时候是2轮	
	{
		//n=1的时候是1轮 2-1,n=2的时候是4-1,n=2的时候是8-1
		int t=pow(2,k);
		for(i=0;i<=t-1;i++)
		for(j=0;j<=t-1;j++)
			a[i+t][j]=a[i][j]+t;
		
		//右上角,绿色区域
		for(i=0;i<=t-1;i++)
		for(j=0;j<=t-1;j++)
			a[i][j+t]=a[i][j]+t;
		
		//右下角,红色区域
		for(i=0;i<=t-1;i++)
		for(j=0;j<=t-1;j++)
			a[i+t][j+t]=a[i][j];
	}	
}

int main()
{
	int n,i,j;
	printf("2^(n+1)个队伍,一共进行了n轮【最后我选它来写程序】,注意n>=0\n");
	scanf("%d",&n);
	
	printf("一共有%d只队伍\n",(int)(pow(2,n+1)));
	
	f(n);
	
	printf("输出对阵表\n");
	for(i=0;i<pow(2,n+1);i++)
	{
		for(j=0;j<pow(2,n+1);j++)
		{
			printf("%3d",a[i][j]);
		}
		printf("\n");
	}
	return 0;
}

吐槽

这道题绝对绝对不可能考

这是一道 循环日程安排问题

1.比起代码,这道题的思路上有着图形题和递归的强烈色彩,与其说是代码,不如说整个思路非常重要
2.这样的解题方式非常局限,可以说很吃这个题目本身,并不算是一个常见的套路、

因此在这里直接不做过多考虑,只需要把握这个有效的递归思路即可,如果有时间我再来更新这个题目

下课内容

关于硬币:关于一个硬币的问题。
如果有16个硬币,都是一元的 重量是6g,其中有1个是假硬币重量是5g,要求找出假的那个?
模仿实现 :
(1)需要一个数组 int a[17];
(2)所有数组一开始全部给初始值6
(3)利用随机函数 生成标号i 范围在1-16之间 a[i]=5
(4)利用分治法方法 来解决问题

思路

首先考虑的是折半查找,但是这样操作必须要解决的是需要排序为前提,而排序就违背了题目本意。
其次从归并考虑,发现想从逆序对处理,返现递归到底合并时候又难计算总的位次,
于是我们从特性入手,
因为5是奇数,所以二分两边的块是否%2!=0,否就二分检索另一块
我们把数组设置为a1到a17,这样中间数就是1+17>>1=9,初次只计算1-8和10-17,以此类推所有的递归都不重不漏。
这样只要检测到mid是奇数,返回即可

注明:

难点感觉完全是边界值处理

AC代码

#include<iostream>
#include<cstdlib>
#include<ctime>

using namespace std;
int a[18];

int search(int l, int r)
{
	int mid = l + r >> 1;
	if (a[mid] & 1)return mid;
	int p1 = 0, p2 = 0;

	for (int i = l; i < mid; i++)p1 += a[i];
	for (int i = mid + 1; i <= r; i++)p2 += a[i];

	if (p1 & 1)search(l, mid);
	else search(mid + 1, r);
}

int main()
{
	for (int i = 1; i < 18; i++)a[i] = 6;

	srand(time(NULL));
	a[rand() % 17 + 1] = 5;

	cout << search(1, 17);

	return 0;
}

自学内容

自学1(查找最大值和次大值)P91页。

事实上在46页

思路

&方便传回值
&同时传值给lmaxFis,lmaxSec,rmaxFis,rmaxSec
分治细化到元素数量为1,更新最大值,
分治细化到元素数量为2,更新最大最小值,
分治细化到元素数量 >2, 比较更新两块的最大最小值

精简写法

#include<iostream>

using namespace std;

int nums[]={2,5,1,4,6,3};
int a,b;

void get(int l,int r,int &a,int &b)
{
    if(l==r)
    {
        if(nums[l]>a)b=a,a=nums[l];
        else b=max(nums[l],b);
        return ;
    }
    int mid = l+r>>1;
    get(l,mid,a,b),get(mid+1,r,a,b);    
}

int main()
{
    get(0,sizeof nums/4,a,b);
    
    cout<<a<<" "<<b;
    
    return 0;
}

了解更多

这道题目的原本代码十分繁杂,宛如裹脚布一般,如果你希望更深一步了解你可以查看我写的[分治]查找最大和次大元素,或者在B站寻找优质视频进行学习

自学2(寻找2个等长有序序列的中位数)P96页。

事实上在第50页

对于一个长度为n的有序序列(假设均为升序序列)a[0…n-1],处于中间位置的元素称为a的中位数。
设计一个算法求给定的两个有序序列的中位数。

代码

如果是单纯为了方便那肯定走这个
但是空间上可能相对较大,如果N存在1E9的数量级那应该就过不了
如果之后有时间再更新另一个方法,有就先用着好了

#include<iostream>
#include<algorithm>

using namespace std;

const int N=1e5;
int t[N];

int main()
{
    int n;
    cin>>n;
    
    for(int i=0;i<2*n;i++)cin>>t[i];
    
    sort(t,t+n);
    
    cout<<t[(0+n-1)>>1];
    
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

俺叫西西弗斯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值