二分算法实例应用(二)

简介: 二分法实例应用(二):和为给定数(POJ 4143)

二分算法实例应用(二)

和为给定数 (POJ 4143)

Description

给出若干个整数,询问其中是否有一对数的和等于给定的数。

Input

共三行:

第一行是整数n(0 < n <= 100,000),表示有n个整数。

第二行是n个整数。整数的范围是在0到10^8之间。

第三行是一个整数m(0 <= m <= 2^30),表示需要得到的和。

Output

若存在和为m的数对,输出两个整数,小的在前,大的在后,中间用单个空格隔开。若有多个数对满足条件,选择数对中较小的数更小的。若找不到符合要求的数对,输出一行No。

Sample Input

4
2 5 1 4
6

Sample Output

1 5




算法思想:

  1. 法一

    首先想到暴力破解,采用两重循环,枚举所有可能的取数方法。

    时间复杂度不难得到为O(n^2)。b

    因为本题问题规模达到了十万,若采用暴力破解则会有100亿的数据规模,基本在所有的OJ中都会超时,所以不可取。

  2. 法二

    既然是要找两个数据满足a+b=m,那么我们可以固定一个值a,在数组中找数m-a,若数组为有序数组,那么在数组中查找一个给定数就可以采用二分查找法来降低时间复杂度。

    因此,

    1. 首先我们需对题中输入数组进行排序,可采用快速排序,时间复杂度为O(n logn);
    2. 其次,依次固定前n-1个数据,对每一个数据a进行一轮循环判断,判断是否有元素为c-a。每一轮判断中可以采用二分查找法进行搜索判定。因此本步时间复杂度为O(n log2 n)。

    综上,总体时间复杂度为O(nlogn)。

  3. 法三

    法三是对法二进行的一个优化,在编写法二的程序时,突然想到:既然都是比较数,那和快速排序中一趟的划分关键步骤岂不是有异曲同工之妙嘛,即将大于枢轴元素的元素移至枢轴右侧,将所有小于枢轴元素的元素移至枢轴左侧。在这样的思想下,产生了对法二的进一步优化。

    即:

    1. 同样需要对输入数据进行排序,采用快速排序,时间复杂度为O(nlogn)。关于排序的时间复杂度在目前能力范围内无法优化。
    2. 对于寻找两个数满足a+b=m这一步,可以考虑:设a为最小元素,即数组左端点;b为最大元素,即数组右端点。

      若(a+b)>m,那么表明,两数之和过大,应该减少最大值,使右端点左移,来减少两数之和。

      若(a+b)<m,那么表明,两数之和过小,应该增大最小值,使左端点右移,来增加两数之和。

      若(a+b)=m,那么便找到了一个解。

      这样的算法在找满足a+b=m的步骤中最多只需遍历一遍数组即可得出解,或判断出是否无解,则本步中时间复杂度为O(n)

    综上,总体时间复杂度为O(nlogn)+O(n)=O(nlogn)。虽然总体时间复杂度没有得到改善,但改善了局部的时间复杂度,比之法二,该算法有更好的性能。




代码逻辑:

  1. 法一

    在算法思想章节中已基本概述清楚,由于本算法并未对数据进行任何排序处理,所以得到的解是无序的,但题中要求输出的两个整数,应小的在前,大的在后,若有多个数对满足条件,选择数对中较小的数更小的。所以在求解时,应求尽所有的解,再判断时候满足题设解的条件。

    具体逻辑较为简单不再赘述,代码中有详细说明。

    代码如下:

    void fun1(int n, int *a, int m) {
        int left = 1e8 + 1, right = 1e8 + 1;//left为组合数据中较小的一个
        for (int i = 0; i < n - 1; i++) {
            for (int j = i + 1; j < n; j++) {
                if (a[i] + a[j] == m) {//成功找到一组数据满足要求
                    if (a[i] < left || a[j] < left) {//判断是否为最小的组合数据
                        if (a[i] < a[j]) {//组合数据中值较小的放左边
                            left = a[i];
                            right = a[j];
                        } else {
                            left = a[j];
                            right = a[i];
                        }
                    }
                    break;
                }
            }
        }
        if (left == 1e8 + 1)
            printf("No");//若无解则输出No
        else
            printf("%d %d", left, right);//优先输出值较小的组合数据
    }
  2. 法二

    1. 首先对输入数组进行快速排序,为便于读着理解,本文采用手写递归快排算法,若读者已经理解快排本质及其运行逻辑,可采用C/C++排序库函数进行排序。
    2. 快排的基本思想是基于分治法的:在待排序的表中任取一个元素pivot作为枢轴元素,通过一趟排序将待排序表划分为两部分,即:左侧,pivot,右侧,左侧元素为所有小于pivot的元素集合,右侧元素为所有大于pivot的元素集合。通过这个一趟快速排序确定了pivot元素在整个数据表中的最终位置,再依次递归的对左侧数据集合进行快排、右侧数据集合进行快排,直至每部分只剩下一个元素或者为空时为止,即表明此时所有元素都放在了最终位置上。

      //快排主函数
      void QuickSort(int a[], int low, int high) {
          if (low < high) {//递归终止条件
              int pivotPos = Partition(a, low, high);//划分一次,同时确定一个数的位置
              QuickSort(a, low, pivotPos - 1);//对该数左边所有元素进行快排
              QuickSort(a, pivotPos + 1, high);//对该数右边边所有元素进行快排
          }
      }

      一趟快排的划分函数的过程,就是一个交替搜索和交换的过程,划分的效率取决于枢轴元素的选取。

      读者可自行通过Data Structure Visualizations工具进行模拟,理解其精髓本质与运行逻辑。

      https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html

      //一趟划分函数
      int Partition(int a[], int low, int high) {
          int pivot = a[low];//将当前表中第一个元素作为枢轴,对表进行划分
          while (low < high) {//循环终止条件
              while (low < high && a[high] >= pivot)high--;//从右往左找第一个小于枢轴元素的元素
              a[low] = a[high];//将小于枢轴元素的元素移至枢轴左侧
              while (low < high && a[low] <= pivot)low++;//从左往右找第一个大于枢轴元素的元素
              a[high] = a[low];//将大于枢轴元素的元素移至枢轴右侧
          }
          a[low] = pivot;//枢轴元素位置确定
          return low;//返回枢轴元素最终位置
      }
    3. 对数据排序完成后,开始依次固定前n-1个数,假设当前为第i个数据元素,此时应对区间[i+1,n]内的数据元素进行二分查找,判断是否有元素满足b=m-a;若有元素满足,则此时的i必为最优解,因为已经对数据进行过排序,i必为最小的解。若区间[i+1,n]内数据元素不存在元素满足b=m-a时,表明当前的第i个元素不为解,应判断下一元素,即i++,直至找到一个解时,退出函数。若当遍历完所以的元素时仍为找到一个解则表明无解。

      代码如下:

      void fun2(int n, int *a, int m) {
          QuickSort(a, 0, n - 1);//将题设数组进行快速排序
          //依次遍历从0~n-1的每一个数据元素,采用二分法查找元素(m - a[i])
          for (int i = 0; i < n - 1; i++) {
              int left = i + 1, right = n;//因为此时为有序数组,所以对区间[i+1,n]进行二分查找
              while (left <= right) {
                  int mid = left + (right - left) / 2;
                  int temp = m - a[i];
      
                  if (temp == a[mid]) {
                      //找到该元素,输出,且由于有序数组,第一个找到的解必为最终解,无须再做法一中的额外判断
                      printf("%d %d", a[i], a[mid]);
                      return;
                  } else if (temp > a[mid]) {//当前中点值小于目标值,修改区间左端点
                      left = mid + 1;
                  } else {//当前中点值大于目标值,修改区间右端点
                      right = mid - 1;
                  }
              }
          }
          printf("No");//若无解则输出No
      }
  3. 法三

    1. 同法二,首先进行快速排序,不再赘述
    2. 对数据排序完成后,由上述算法思想可知,应记录解的区间的左右端点,left=0,right=n-1;通过判断左右端点的元素之和是否与m相等来确定是否为解,记temp=a[left] + a[right];

      • 若 temp = m 则表明找到了解,应输出解
      • 若 temp > m 则表明左右端点之和过大,应减少较大的值,即将右端点左移
      • 若 temp < m 则表明左右端点之和过小,应增加较小的值,即将左端点右移

        每次第边界移动后修改temp的值,再循环判断是否满足 temp = m ,直至找到一个解。

      代码如下:

      //借用快排中划分方法中的思想对法二进行进一步优化
      void fun3(int n, int *a, int m) {
          QuickSort(a, 0, n - 1);//将题设数组进行快速排序
      
          int left = 0, right = n - 1;//记录可能的值的左右区间端点
          int temp = a[left] + a[right];//temp记录区间左右端点元素的和
          while (left < right && (temp != m)) {//若区间合理且temp≠m,进行边界移动
              if (temp > m) {//若temp>m则表明左右端点之和过大,应减少较大的值,即将右端点左移
                  right--;
              }
              if (temp < m) {//若temp<m则表明左右端点之和过小,应增加较小的值,即将左端点右移
                  left++;
              }
              temp = a[left] + a[right];//修改左右端点之和用作下一次判断
          }
      
          if (left < right) {//若left < right,则表明找到了一个区间的左右端点使得其和temp=m;
              printf("%d %d", a[left], a[right]);
          } else {//若left < right,则表明无解,则输出No
              printf("No");
          }
      }




代码整合:

//
// Created by Ss1Two on 2023/2/7.
//

#include "stdio.h"


void fun1(int n, int *a, int m);//法一

void fun2(int n, int *a, int m);//法二

void fun3(int n, int *a, int m);//法三

void QuickSort(int a[], int low, int high);//快速排序函数声明

int Partition(int a[], int low, int high);//快速排序一趟划分函数声明

int main(void) {
    int n, arr[100000], m;
    scanf_s("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf_s("%d", &arr[i]);
    }
    scanf_s("%d", &m);


//    fun1(n, arr, m);
//    fun2(n, arr, m);
    fun3(n, arr, m);
    return 0;
}

void fun1(int n, int *a, int m) {
    int left = 1e8 + 1, right = 1e8 + 1;//left为组合数据中较小的一个
    for (int i = 0; i < n - 1; i++) {
        for (int j = i + 1; j < n; j++) {
            if (a[i] + a[j] == m) {//成功找到一组数据满足要求
                if (a[i] < left || a[j] < left) {//判断是否为最小的组合数据
                    if (a[i] < a[j]) {//组合数据中值较小的放左边
                        left = a[i];
                        right = a[j];
                    } else {
                        left = a[j];
                        right = a[i];
                    }
                }
                break;
            }
        }
    }
    if (left == 1e8 + 1)
        printf("No");//若无解则输出No
    else
        printf("%d %d", left, right);//优先输出值较小的组合数据
}

void fun2(int n, int *a, int m) {
    QuickSort(a, 0, n - 1);//将题设数组进行快速排序
    //依次遍历从0~n-1的每一个数据元素,采用二分法查找元素(m - a[i])
    for (int i = 0; i < n - 1; i++) {
        int left = i + 1, right = n;//因为此时为有序数组,所以对区间[i+1,n]进行二分查找
        while (left <= right) {
            int mid = left + (right - left) / 2;
            int temp = m - a[i];

            if (temp == a[mid]) {
                //找到该元素,输出,且由于有序数组,第一个找到的解必为最终解,无须再做法一中的额外判断
                printf("%d %d", a[i], a[mid]);
                return;
            } else if (temp > a[mid]) {//当前中点值小于目标值,修改区间左端点
                left = mid + 1;
            } else {//当前中点值大于目标值,修改区间右端点
                right = mid - 1;
            }
        }
    }
    printf("No");//若无解则输出No
}

//借用快排中划分方法中的思想对法二进行进一步优化
void fun3(int n, int *a, int m) {
    QuickSort(a, 0, n - 1);//将题设数组进行快速排序

    int left = 0, right = n - 1;//记录可能的值的左右区间端点
    int temp = a[left] + a[right];//temp记录区间左右端点元素的和
    while (left < right && (temp != m)) {//若区间合理且temp≠m,进行边界移动
        if (temp > m) {//若temp>m则表明左右端点之和过大,应减少较大的值,即将右端点左移
            right--;
        }
        if (temp < m) {//若temp<m则表明左右端点之和过小,应增加较小的值,即将左端点右移
            left++;
        }
        temp = a[left] + a[right];//修改左右端点之和用作下一次判断
    }

    if (left < right) {//若left < right,则表明找到了一个区间的左右端点使得其和temp=m;
        printf("%d %d", a[left], a[right]);
    } else {//若left < right,则表明无解,则输出No
        printf("No");
    }
}

//快排主函数
void QuickSort(int a[], int low, int high) {
    if (low < high) {//递归终止条件
        int pivotPos = Partition(a, low, high);//划分一次,同时确定一个数的位置
        QuickSort(a, low, pivotPos - 1);//对该数左边所有元素进行快排
        QuickSort(a, pivotPos + 1, high);//对该数右边边所有元素进行快排
    }
}

//一趟划分
int Partition(int a[], int low, int high) {
    int pivot = a[low];//将当前表中第一个元素作为枢轴,对表进行划分
    while (low < high) {//循环终止条件
        while (low < high && a[high] >= pivot)high--;//从右往左找第一个小于枢轴元素的元素
        a[low] = a[high];//将小于枢轴元素的元素移至枢轴左侧
        while (low < high && a[low] <= pivot)low++;//从左往右找第一个大于枢轴元素的元素
        a[high] = a[low];//将大于枢轴元素的元素移至枢轴右侧
    }
    a[low] = pivot;//枢轴元素位置确定
    return low;//返回枢轴元素最终位置
}

Created by Ss1Two on 2023/2/7

目录
相关文章
|
17天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
35 3
|
23天前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习中的优化算法及其应用
【10月更文挑战第8天】 本文将探讨深度学习中常用的优化算法,包括梯度下降法、Adam和RMSProp等,介绍这些算法的基本原理与应用场景。通过实例分析,帮助读者更好地理解和应用这些优化算法,提高深度学习模型的训练效率与性能。
120 63
|
12天前
|
存储 算法 搜索推荐
这些算法在实际应用中有哪些具体案例呢
【10月更文挑战第19天】这些算法在实际应用中有哪些具体案例呢
21 1
|
18天前
|
机器学习/深度学习 人工智能 算法
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
52 0
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
|
22小时前
|
机器学习/深度学习 JSON 算法
二叉树遍历算法的应用场景有哪些?
【10月更文挑战第29天】二叉树遍历算法作为一种基础而重要的算法,在许多领域都有着不可或缺的应用,它为解决各种复杂的问题提供了有效的手段和思路。随着计算机科学的不断发展,二叉树遍历算法也在不断地被优化和扩展,以适应新的应用场景和需求。
4 0
|
26天前
|
算法 安全 物联网
如何应用SM2算法进行身份认证
【10月更文挑战第5天】如何应用SM2算法进行身份认证
41 1
|
26天前
|
存储 算法 安全
SM2算法的应用场景有哪些?
【10月更文挑战第5天】SM2算法的应用场景有哪些?
48 1
|
30天前
|
存储 算法 安全
Python 加密算法详解与应用
Python 加密算法详解与应用
19 1
|
1月前
|
机器学习/深度学习 算法
深度学习中的优化算法及其应用
本文探讨了深度学习中常用的优化算法,包括梯度下降、随机梯度下降、动量方法和Adam方法。通过对比这些算法的优缺点及适用场景,帮助读者更好地理解和应用这些优化方法。
25 2
|
12天前
|
监控 算法 数据挖掘
HyperLogLog算法有哪些应用场景呢
【10月更文挑战第19天】HyperLogLog算法有哪些应用场景呢
11 0