算法设计与实现--分治篇

什么是分治算法

分治算法是一种常见的问题解决方法,它将一个复杂的问题划分为多个相同或相似的子问题,然后递归地解决这些子问题,最后将子问题的解合并得到原问题的解。

分治算法通常包含三个步骤:

  1. 分解(Divide):将原问题划分成多个相同或相似的子问题。这里的划分策略取决于具体问题的特性。

  2. 解决(Conquer):递归地解决子问题。对于规模较小的子问题,可以直接求解。

  3. 合并(Combine):将子问题的解合并成原问题的解。这一步骤通常在递归的回溯过程中完成。

分治算法的核心思想是将复杂的问题划分为规模更小的子问题,并通过递归地解决这些子问题来解决原问题。通过合并子问题的解,最终得到原问题的解。

分治算法常用于解决一些具有重叠子问题性质的问题,例如归并排序、快速排序和求解最大子数组等。它可以提高问题的求解效率,降低问题的复杂度。

然而,分治算法并非适用于所有类型的问题。在应用分治算法时,需要满足以下条件:

  1. 原问题可以划分为多个规模较小的子问题。
  2. 子问题的解可以合并成原问题的解。
  3. 子问题之间相互独立,即子问题之间没有重叠部分。

通过合理地划分问题、解决子问题和合并子问题的解,分治算法可以帮助我们有效地解决各种复杂的问题。

常见的用分治算法解决的问题

1、归并排序

归并排序是一种常见的排序算法,其基本思想是将待排序的数组分成两个子数组,分别对这两个子数组进行递归排序,然后将两个有序的子数组合并成一个有序的数组。下面是归并排序的算法思路

  1. 分解:将待排序的数组分解为两个子数组。选择一个中间位置(mid)将数组一分为二,分别得到左子数组和右子数组。

  2. 递归排序:对左子数组和右子数组分别进行递归排序。将这一步骤递归地应用于左子数组和右子数组,直到子数组的长度为1或0,即无法再分解。

  3. 合并:将两个有序的子数组合并成一个有序的数组。创建一个临时数组(tmp)来存储合并后的结果。从两个子数组的起始位置开始,比较两个子数组的元素,将较小的元素放入临时数组中,并将对应子数组的指针向后移动一位。重复这个过程直到其中一个子数组的所有元素都被放入临时数组中。将剩余的子数组中的元素依次放入临时数组的末尾。最后,将临时数组中的元素复制回原始数组的对应位置。

  4. 递归结束:当子数组的长度为1或0时,递归结束。此时,数组已经有序。

代码实现

/*算法思路:首先我们需要将一个待排序的序列分成若干个子序列,我们采用2分法,在中间(mid)处划分成了两个序列1、我们采用递归思想,将一个序列在mid处划分,直到不能划分为止
2、将划分的子序列进行排序,然后将排序好的子序列合并*/void merge(int a[],int start,int mid,int end){int *tmp = (int *)malloc((end-start+1)*sizeof(int));    // 动态分配一个临时存放数组int i = start;int j = mid+1;int k = 0;// 排序while(i<=mid && j<=end){if(a[i]<=a[j]){tmp[k++] = a[i++];}else{tmp[k++] = a[j++];}}// 如果还剩下序列,直接加到数组最后面while(i<=mid){tmp[k++] = a[i++];}while(j<=end){tmp[k++] = a[j++];}// 最后合并子序列for(int i=0;i<k;i++){a[start+i] = tmp[i]; }free(tmp);}void merge_sort(int a[],int start,int end){if(a==NULL || start>=end){return ;}// 递归划分子序列int mid = (start+end)/2;merge_sort(a,start,mid);merge_sort(a,mid+1,end);// 合并排序好的子序列merge(a,start,mid,end);
}

2、最大子数组问题

最大子数组问题是一个经典的问题,其目标是在给定数组中找到一个子数组,使得该子数组的元素和最大

  1. 基本情况:如果数组的长度为1,直接返回该元素作为最大子数组。

  2. 分解:将原数组划分为两个子数组。找到数组的中间位置,将数组一分为二,分别得到左子数组和右子数组。

  3. 递归求解:对左子数组和右子数组分别进行递归求解最大子数组。即对左子数组和右子数组分别调用最大子数组算法。

  4. 跨越中点的最大子数组:考虑包含中点的子数组,即左子数组的最右边元素和右子数组的最左边元素之间的连续子数组。从中点开始向左扩展,记录最大和,然后从中点开始向右扩展,记录最大和。将这两个和相加,得到跨越中点的最大子数组和。

  5. 合并:比较左子数组、右子数组和跨越中点的最大子数组的和,取最大的作为最终的最大子数组和。

  6. 返回结果:返回最大子数组的起始位置、结束位置和和值。

代码实现

#include <stdio.h>
#include <limits.h>// 求解跨越中点的最大子数组和
int maxCrossingSum(int arr[], int low, int mid, int high) {int leftSum = INT_MIN; // 表示无穷小,因为负数也要进行求和运算 int sum = 0;// 求出左边子序列的最大值 for (int i = mid; i >= low; i--) {sum += arr[i];if (sum > leftSum) {leftSum = sum;}}int rightSum = INT_MIN;sum = 0;// 求右边子序列的最大值 for (int i = mid + 1; i <= high; i++) {sum += arr[i];if (sum > rightSum) {rightSum = sum;}}return leftSum + rightSum;
}// 分治法求解最大子数组和
int maxSubArrayUtil(int arr[], int low, int high) {// 当子序列只有一个元素时if (low == high) {return arr[low];}int mid = (low + high) / 2;// 最大子数组和可能在左边、右边或跨越中点int leftMax = maxSubArrayUtil(arr, low, mid);int rightMax = maxSubArrayUtil(arr, mid + 1, high);// 合并之后的值int crossMax = maxCrossingSum(arr, low, mid, high);// 返回三者中的最大值// condition ? expression_if_true : expression_if_false;// condition 条件为真,则返回 expression_if_true,否则返回expression_if_falsereturn (leftMax > rightMax && leftMax > crossMax) ? leftMax : ((rightMax > leftMax && rightMax > crossMax) ? rightMax : crossMax);
}// 对外接口,调用分治法求解最大子数组和
int maxSubArray(int arr[], int numsSize) {return maxSubArrayUtil(arr, 0, numsSize - 1);
}int main() {int nums[] = {1,-2,4,5,-2,8,3,-2,6,7,3,-1};int numsSize = sizeof(nums) / sizeof(nums[0]);int result = maxSubArray(nums, numsSize);printf("子数组最大和为: %d\n", result);return 0;
}

在这里会有一个问题,就是如果你输入的数组 num = [-1,-1,-2,-2]时,你返回的结果是-2,但其实真正的最大值是-1,所以分治算法并不是解决最大子数组问题的最优解,在这个给大家分享另一种算法思路,那就是利用动态规划的算法思想来解决问题。

思路:

1、定义一个动态规划数组 D,其中 D[i] 表示以第 i 个元素结尾的最大子数组和。

2、递推关系是关键,它告诉我们如何通过已知信息计算新的信息:D[i] = max(D[i-1] + num[i], num[i])

代码如下

# include <stdio.h>int max(int a,int b){if(a>b){return a;}else{return b;}
}int maxSubArray(int* num,int size){if(size == 0){return 0;}int D[size];D[0] = num[0];  // 动态规划数组,D[i]表示以第i个元素结尾的最大子数组和int maxSum = D[0];  // 初始化第一个元素// 从第二个元素开始遍历数组,根据递推关系更新动态规划数组for(int i = 1;i<size;i++){D[i] = max((D[i-1]+num[i]),num[i]);if(maxSum < D[i]){maxSum = D[i];}}return maxSum;
}int main(){int nums[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4};int size = sizeof(nums) / sizeof(nums[0]);int result = maxSubArray(nums, size);printf("Maximum Subarray Sum: %d\n", result);return 0;
} 

3、逆序对问题

逆序对问题是一个经典的问题,其目标是计算一个数组中逆序对的数量。逆序对指的是数组中的两个元素,若其在数组中的相对顺序与排序后的顺序相反,则它们构成一个逆序对。

例如num = [5,2,3,6,8] 其中num[0] = 5,num[1] = 2, 这就是一个逆序对。

下面是逆序对问题的一种常见算法思路,基于归并排序的思想:

  1. 分解:将原数组划分为两个相等的子数组,直到子数组的长度为 1。这可以通过递归实现。

  2. 递归求解:对左子数组和右子数组分别进行递归求解逆序对的数量。

  3. 合并与计数:在合并左右子数组的过程中,统计逆序对的数量。合并时,使用两个指针分别指向左子数组和右子数组的起始位置,并进行比较。若右子数组当前元素小于左子数组当前元素,则构成逆序对,并将逆序对的数量累加。同时,将较小的元素放入合并后的数组中,移动对应的指针。若左子数组或右子数组遍历完毕,则将剩余的元素直接放入合并后的数组中。

  4. 返回结果:返回逆序对的数量。

代码实现

# include <stdio.h>
# include<stdlib.h>// 归并排序辅助函数:将两个有序数组合并为一个有序数组,并计算逆序对个数
int  merge(int* num,int low,int mid,int high){int count = 0;int* tmp = (int *)malloc((high-low+1)*sizeof(int));int i = low;int j = mid+1;int k = 0;while(i<=mid && j<=high){if(num[i]<=num[j]){tmp[k++] = num[i++];}else{tmp[k++] = num[j++];count += (mid-i+1); //  左数组剩余元素都大于arr[j],构成逆序对}}while(i<=mid){tmp[k++] = num[i++];}while(j<=high){tmp[k++] = num[j++];}// 将合并后的结果拷贝回原数组for(int m = 0;m<k;m++){num[low+m] = tmp[m];}free(tmp);return count;
}// 归并排序
int merge_sort(int* num,int low,int high){int count = 0;if(num == NULL || low >= high){return 0;} int mid = (low+high)/2;//  分别对左右两个子数组进行归并排序,并累加逆序对个数count += merge_sort(num,low,mid);count += merge_sort(num,mid+1,high);// 合并两个有序数组,并累加逆序对个数count += merge(num,low,mid,high);return count;} // 求解逆序对
int countInversePairs(int* num ,int length) {int count = merge_sort(num,0,length-1);return count;}int main(){
//	int arr[] = {13,8,10,6,15,18,12,20,9,14,17,19};int arr[] = {4,6,8,3,5};
//	int arr[] = {9, 5, 7, 2, 4, 1, 6, 8, 3};int length = sizeof(arr) / sizeof(arr[0]);int count = countInversePairs(arr,length);printf("%d",count);return 0;
} 

4、快速排序

快速排序是一种常用的排序算法,其基于分治思想进行排序。下面是快速排序的算法思路:

  1. 选择基准:从数组中选择一个元素作为基准(pivot)。一般可以选择第一个元素、最后一个元素或者随机选择一个元素作为基准。

  2. 分区操作:将数组中的元素按照与基准的大小关系划分为两个子数组,一个子数组中的元素小于等于基准,另一个子数组中的元素大于基准。可以通过使用两个指针,一个指向数组的起始位置,一个指向数组的结束位置,通过交换元素来实现分区操作。

    • 初始化两个指针 i 和 j,分别指向数组的起始位置和结束位置。
    • 从指针 i 开始向右移动,直到找到一个元素大于基准。
    • 从指针 j 开始向左移动,直到找到一个元素小于基准。
    • 如果 i 小于等于 j,交换指针 i 和 j 所指向的元素。
    • 继续移动指针,直到 i 大于 j
  3. 递归排序:对基准左边的子数组和右边的子数组分别进行递归排序。即,对左子数组和右子数组重复上述步骤,直到子数组的长度为 1 或 0。

  4. 合并结果:递归排序完成后,数组已经有序。

代码实现

# include<stdio.h>//  交换数组中两个元素的值
void swap(int* a,int* b){int tmp = *a;*a = *b;*b = tmp;
}// 划分函数,返回基准元素的最终位置
int partition(int* num,int low,int high){int pivot = num[high]; // 选择最后一个元素作为基准元素int i = low - 1;  //  i表示小于基准的元素的右边界for(int j = low;j<=high-1;j++){if(num[j]<=pivot){i++;swap(&num[i],&num[j]);  // 将小于等于基准的元素交换到左侧}} swap(&num[i+1],&num[high]);  // 将基准元素放到正确的位置return i+1;    // 返回基准元素的最终位置} // 快速排序函数
void Quick_sort(int* num,int low,int high){if(low<high){int pivotIndex = partition(num,low,high);  // 划分数组,获取基准元素的位置递归对基准元素左侧和右侧的子数组进行排序Quick_sort(num,low,pivotIndex-1);Quick_sort(num,pivotIndex+1,high);}
} int main(){int num[] = {9, 5, 7, 2, 4, 1, 6, 8, 3};int length = sizeof(num) / sizeof(num[0]); Quick_sort(num, 0, length - 1);printf("排序结果:");for (int i = 0; i < length; i++) {printf("%d ", num[i]);}printf("\n");return 0;
} 

5、次序选择问题

次序选择问题(Order Statistics Problem)是指从一个未排序的数组中找到第 k 小(或第 k 大)的元素。下面是一种常见的算法思想,基于快速排序的分治思想来解决次序选择问题:这个可以看作快速排序的应用。

  1. 选择基准:从数组中选择一个元素作为基准(pivot)。一般可以选择第一个元素、最后一个元素或者随机选择一个元素作为基准。

  2. 分区操作:将数组中的元素按照与基准的大小关系划分为两个子数组,一个子数组中的元素小于等于基准,另一个子数组中的元素大于基准。可以通过使用两个指针,一个指向数组的起始位置,一个指向数组的结束位置,通过交换元素来实现分区操作。

    • 初始化两个指针 i 和 j,分别指向数组的起始位置和结束位置。
    • 从指针 i 开始向右移动,直到找到一个元素大于基准。
    • 从指针 j 开始向左移动,直到找到一个元素小于基准。
    • 如果 i 小于等于 j,交换指针 i 和 j 所指向的元素。
    • 继续移动指针,直到 i 大于 j
  3. 递归选择:根据分区操作后基准的位置,将问题转化为在基准的左边或右边子数组中寻找第 k 小(或第 k 大)的元素。若基准的位置为 k-1,则找到了第 k 小(或第 k 大)的元素;若基准的位置大于 k-1,则在左边子数组中递归选择第 k 小(或第 k 大)的元素;若基准的位置小于 k-1,则在右边子数组中递归选择第 k-j 小(或第 k-j 大)的元素。

  4. 返回结果:最终返回第 k 小(或第 k 大)的元素。

代码实现

#include <stdio.h>// 交换数组中两个元素的值
void swap(int* a, int* b) {int temp = *a;*a = *b;*b = temp;
}// 划分函数,返回基准元素的最终位置
int partition(int arr[], int low, int high) {int pivot = arr[low];  // 选择第一个元素作为基准元素int i = low;  // i表示小于基准的元素的右边界for (int j = low + 1; j <= high; j++) {if (arr[j] < pivot) {i++;swap(&arr[i], &arr[j]);  // 将小于基准的元素交换到左侧}}swap(&arr[low], &arr[i]);  // 将基准元素放到正确的位置return i;  // 返回基准元素的最终位置
}// 快速选择函数
int quickSelect(int arr[], int low, int high, int k) {if (low == high) {return arr[low];  // 当数组只有一个元素时,直接返回该元素}int pivotIndex = partition(arr, low, high);  // 划分数组,获取基准元素的位置if (k == pivotIndex) {return arr[k];  // 基准元素即为第k小(或第k大)的元素} else if (k < pivotIndex) {return quickSelect(arr, low, pivotIndex - 1, k);  // 在基准元素的左侧递归查找} else {return quickSelect(arr, pivotIndex + 1, high, k);  // 在基准元素的右侧递归查找}
}int main() {下·int arr[] = {9, 5, 7, 2, 4, 1, 6, 8, 3};int length = sizeof(arr) / sizeof(arr[0]);int k = 4;  // 查找第4小的元素int result = quickSelect(arr, 0, length - 1, k - 1);printf("第%d小的元素是:%d\n", k, result);return 0;
}

总结一下,其实分治思想就是一个将原问题化成多个子问题的算法思想,其中有三个重要的步骤

分解原问题:将原问题分解成多个子问题

解决子问题:递归求解各个子问题

合并问题解:将结果合并为原问题解

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

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

相关文章

MongoDB的部署

MongoDB部署 基于Linux平台 前置要求 在Centos7 64位系统中安装64位的MongoDB的安装包&#xff0c;通过访问MongoDB官网https://www.mongodb.com/download-center/community进入MongoDB下载页面。Version&#xff1a;指定MongoDB版本&#xff0c;MongoDB的版本分为稳定版和开发…

5.3每日一题(不确定正负号的级数敛散性:和一个正项级数比较判定)

比较判别法和比较判别法的极限形式是对正项级数而言的&#xff0c;若一个级数和p级数比较&#xff0c;结果>0&#xff0c;则同敛散&#xff1b;若结果<0&#xff0c;则结果乘以-1 结果又同敛散了&#xff1b;所以只要比值不等于0&#xff0c;则同敛散&#xff1b; 所以当…

JVM虚拟机:G1垃圾回收器的日志分析

本文重点 本文我们将学习G1垃圾回收器的日志 使用 执行命令 java -Xms20M -Xmx20M -XX:PrintGCDetails -XX:UseG1GC 类名 分析 前面我们学习了G1垃圾回收器&#xff0c;它的回收有三种可能&#xff1a; YGC FGC MixedGC GC pause表示STW,Evacuation表示复制对象&#xff0c;…

c语言-字符函数和字符串函数详解

文章目录 1. 字符分类函数2. 字符转换函数3. strlen的使用和模拟实现4. strcpy的使用和模拟实现5. strncpy函数的使用6. strcat的使用和模拟实现7. strncat函数的使用8. strcmp的使用和模拟实现9. strncmp函数的使用10. strstr的使用和模拟实现11. strtok函数的使用12. strerro…

【尚跑】2023宝鸡马拉松安全完赛,顺利PB达成

1、赛事背景 千年宝地&#xff0c;一马当先&#xff01;10月15日7时30分&#xff0c;吉利银河2023宝鸡马拉松在宝鸡市行政中心广场鸣枪开跑。 不可忽视的是&#xff0c;这次赛事的卓越之处不仅在于规模和参与人数&#xff0c;还在于其精心的策划和细致入微的组织。为了确保每位…

Kotlin学习——kt里面的函数,高阶函数 函数式编程 扩展函数和属性

Kotlin 是一门现代但已成熟的编程语言&#xff0c;旨在让开发人员更幸福快乐。 它简洁、安全、可与 Java 及其他语言互操作&#xff0c;并提供了多种方式在多个平台间复用代码&#xff0c;以实现高效编程。 https://play.kotlinlang.org/byExample/01_introduction/02_Functio…

论文笔记:详解NEUPSL DSI

《Using Domain Knowledge to Guide Dialog Structure Induction via Neural Probabilistic 》 名词解释 Dialog Structure Induction&#xff08;DSI&#xff09;是推断给定目标导向对话的潜在对话结构&#xff08;即一组对话状态及其时间转换&#xff09;的任务。它是现代对…

基于Scapy修改ClientHello的SNI(三)

需求:修改HTTPS的ClientHello中的SNI字段 目标:修改成功,wireshark显示正常 语言:Python 三方库:Scapy 下面是一个标准的ClientHello报文,是从一个完整的HTTPS流中保存出来的,原始报文中的SNI是www.baidu.com 在上一篇文章中 记录基于scapy构造ClientHello报文的尝试…

【Docker】Docker与Kubernetes:区别与优势对比

前言 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。   kubernetes&#xff0c;简称K8s&a…

C#调用ffmpeg从视频提取图片

微信公众号“CSharp编程大全”的文章《C#从视频提取图片&#xff1f;》介绍了基于Microsoft.DirectX.AudioVideoPlayback.Video类实现从视频提取图片的方式&#xff0c;本来是想学习并测试该类的用法&#xff0c;但实际测试过程中却没有测通。百度从视频提取图片&#xff0c;网…

【Leetcode合集】1457. 二叉树中的伪回文路径

1457. 二叉树中的伪回文路径 1457. 二叉树中的伪回文路径 代码仓库地址&#xff1a; https://github.com/slience-me/Leetcode 个人博客 &#xff1a;https://slienceme.xyz 给你一棵二叉树&#xff0c;每个节点的值为 1 到 9 。我们称二叉树中的一条路径是 「伪回文」的&am…

rtsp点播异常出现‘circluar_buffer_size‘ option was set but it is xx

先说现象: 我使用potplay播放器来点播rtsp码流的时候可以点播成功&#xff0c;同事使用vlc和FFplay来点播rtsp码流的时候异常。 排查思路: 1.开始怀疑是oss账号问题&#xff0c;因为ts切片数据是保存在oss中的&#xff0c;我使用的是自己的oss账号&#xff0c;同事使用的是公司…

四边形不等式优化DP

目录 四边形不等式内容[HNOI2008]玩具装箱解析代码实现 参考资料 四边形不等式内容 TODO [HNOI2008]玩具装箱 解析 满足四边形不等式&#xff0c;决策具有单调性. 对于两个位置 i , j i, j i,j, 对应的最优决策点一定有 o p t [ i ] < o p t [ j ] opt[i] < opt[j]…

代码随想录 406. 根据身高重建队列

题目 假设有打乱顺序的一群人站成一个队列&#xff0c;数组 people 表示队列中一些人的属性&#xff08;不一定按顺序&#xff09;。每个 people[i] [hi, ki] 表示第 i 个人的身高为 hi &#xff0c;前面 正好 有 ki 个身高大于或等于 hi 的人。 请你重新构造并返回输入数组 p…

windows中redis闪退问题

windows下Redis启动闪退问题解决经验汇总_redis闪退-CSDN博客 结论:redis的版本太低了,需要重新下载最新版本

芯片安全和无线电安全底层渗透技术

和传统网络安全不同&#xff0c;硬件安全、芯片安全、无线电安全属于网络底层安全的重要细分领域&#xff0c;是网络安全的真正基石&#xff0c;更是国家安全的重要组成部分&#xff0c;“夯实网络底层安全基础&#xff0c;筑牢网络强国安全底座”&#xff0c;是底网安全重要性…

【小沐学写作】原型设计工具汇总(Axure RP)

文章目录 1、简介2、Axure RP2.1 工具简介2.2 工具特点2.2.1 互动事件2.2.2 条件逻辑2.2.4 工作表格2.2.5 多状态容器2.2.6 数据驱动接口2.2.7 自适应视图2.2.8 流程图 2.3 工具安装2.3.1 安装2.3.2 运行 2.4 使用费用2.5 工具体验2.5.1 登陆框制作 3、其他3.1 Figma3.2 Adobe …

如何通过ShardingJDBC进行读写分离

背景信息&#xff1a; 面对日益增加的系统访问量&#xff0c;数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说&#xff0c;将数据库拆分为主库和从库。其中主库负责处理事务性的增删改操作&#xff0c;从库负责处理查询操作&#…

实验题【网关设置+VRRP+OSPF】(H3C模拟器)

嘿&#xff0c;这里是目录&#xff01; ⭐ H3C模拟器资源链接1. 实验示意图2. 要求和考核目标3. 当前配置3.1 PC1、PC2、PC3、PC4和PC5配置3.2 SW配置3.2.1 SW2配置3.2.2 SW3配置3.2.3 SW4配置3.2.4 SW1配置 3.2. R配置3.2.1 R1配置3.2.2 R2配置 ⭐ H3C模拟器资源链接 H3C网络…

微服务学习|初识MQ、RabbitMQ快速入门、SpringAMQP

初识MQ 同步通讯和异步通讯 同步通讯是实时性质的&#xff0c;就好像你用手机与朋友打视频电话&#xff0c;但是&#xff0c;别人再想与你视频就不行了&#xff0c;异步通讯不要求实时性&#xff0c;就好像你用手机发短信&#xff0c;好多人都能同时给你发短信&#xff0c;你…