算法笔记 04 —— 算法初步(下)

本系列为胡凡编著的算法笔记当中代码部分的精简版整理,笔者也在同时准备Leetcode刷题和实习面试,希望为有一定编码和数据结构基础的同学提供一份系统型的参考,以方便遗忘时的算法查阅、期末复习总览以及C++学习参照。

目录

[01 贪心](#01 贪心)

Ⅰ简单贪心

[Ⅱ 区间贪心](#Ⅱ 区间贪心)

[02 二分](#02 二分)

[Ⅰ 二分查找](#Ⅰ 二分查找)

[Ⅱ 二分法拓展](#Ⅱ 二分法拓展)

[Ⅲ 快速幂](#Ⅲ 快速幂)

[03 Two Pointers](#03 Two Pointers)

[Ⅰ 什么是 two points?](#Ⅰ 什么是 two points?)

[Ⅱ 归并排序](#Ⅱ 归并排序)

[Ⅲ 快速排序](#Ⅲ 快速排序)

[04 打表](#04 打表)

[05 递推](#05 递推)

[06 随机选择算法](#06 随机选择算法)


01 贪心

Ⅰ简单贪心
cpp 复制代码
#include <cstdio>
#include <algorithm>
using namespace std;

struct moonCake{
    double store;
    double sell;
    double price;
}moon[1010]

bool cmp(moon a, moon b){
    return a.price > b.price;
}

int maxSell(double D){
    sort(moon, moon+N, cmp);
    
    double num = 0;
    for(int i=0; i<N; i++){
        if(moon[i].store > D){ //供大于求
            num = num + moon[i].price * D;
            return num;
        }else{ //供不应求
            num = num + moon[i].sell;
            D = D - moon[i].store;
        }
    }
    return num;
}

int main(){
    int N;
    double D;
    scanf("%d %lf", &N, &D);
    
    for(int i=0; i<N; i++){
        scanf("%lf", &moon[i].store);
    }
    for(int i=0; i<N; i++){
        scanf("%lf", &moon[i].sell);
        moon[i].price = moon[i].sell / moon[i].store;
    }
    
    printf("%.2f", maxSell(D));
    return 0;
}
cpp 复制代码
#include <cstdio>
int main(){
    int count[10];
    for(int i=0; i<10; i++){
        scanf("%d", &count[i]);
    }
    
    //输出第一位非0数字
    for(int i=1; i<10; i++){
        if(count[i] > 0){
            printf("%d", i);
            count[i]--;
            break;
        }
    }
    
    //输出剩下的数字
    for(int i=0; i<10; i++){
        for(int j=0; j<count[i]; j++){
            printf("%d", i);
        }
    }
}
Ⅱ 区间贪心

区间不相交问题:

给出 N 个开区间 (x, y),从中选择尽可能多的开区间,使得这些开区间两两没有交集。

cpp 复制代码
//区间贪心
//struct
//cmp

#include <cstdio>
#include <algorithm>
using namespace std;

const int maxn = 110;
struct Interval{
    int x;
    int y;
}I[maxn];

bool cmp(Interval a, Interval b){
    if(a.x != b.x) return a.x > b.x;
    else return a.y < b.y;
}

int main(){
    int n;
    while(scanf("%d", &n), n != 0){
        for(int i=0; i<n; i++){
            scanf("%d%d", &I[i].x, &I[i].y);
        }
        sort(I, I+n, cmp);
        
        int ans = 1;
        int lastX = I[0].x;
        for(int i=1; i<n; i++){
            if(I[i].y < lastX){
                lastX = I[i].x;
                ans++;
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

02 二分

Ⅰ 二分查找
cpp 复制代码
#include <stdio.h>
const int N = 10;
//A[]为严格递增数列,left为二分下界,right为二分上界,x为欲查询的数
//二分区间为左闭右闭的[left, right],传入的初值为[0, N-1]

//函数返回等于 x 的元素位置
int binarySearch(int A[], int left, int right, int x){
    int mid;
    while(left < right){
        mid = (left + right) / 2
        if(A[mid] == x){
            return mid;
        }
        else if(A[mid] < x){
            left = mid + 1;
        }
        else{
            right = mid - 1;
        }
    }
    return -1;
}

int main(){
    int A[N] = {1, 5, 8, 12, 13, 26, 32, 54, 58, 68};
    int num = binarySearch(A, 0, N-1, 8);
    printf("%d\n", num);
    return 0;
}
cpp 复制代码
#include <stdio.h>
const int N = 10;
//A[]为严格递增数列,left为二分下界,right为二分上界,x为欲查询的数
//二分区间为左闭右闭的[left, right],传入的初值为[0, N-1]

//函数返回第一个大于等于 x 的元素位置
int binarySearch(int A[], int left, int right, int x){
    int mid;
    while(left < right){
        mid = (left + right) / 2
        if(A[mid] == x){
            return mid;
        }
        else if(A[mid] < x){
            left = mid + 1;
        }
        else{
            right = mid; //mid 可能是第一个大于等于 x 的元素
        }
    }
    return -1;
}

int main(){
    int A[N] = {1, 5, 8, 12, 13, 26, 32, 54, 58, 68};
    int num = binarySearch(A, 0, N-1, 11);
    printf("%d\n", num);
    return 0;
}
Ⅱ 二分法拓展

① 计算根号 2 的近似值。

cpp 复制代码
const double eps = le-5; //精度为 10^-5
double f(double x){
    return x * x
}

double calSqrt(double left, double right, double num){ //num
    double mid;
    while(right - left > eps){ //左右区间长度小于eps
        mid = (left + right) / 2;
        if(f(mid) < num){
            left = mid;
        }
        else{
            right = mid;
        }
    }
    return mid;
}

② 有一个侧面看去是半圆的储水装置,该半圆的半径为 R,要求往里面装入高度为 h 的水,使其在侧面看去的面积与半圆面积的比例恰好为r,现在给定 R 和 r ,求高度 h。

思路:在 [0, R] 范围内对水面高度 h 进行二分,计算在高度下面积比例 r 的值。如果计算得到的r比给定的数值要大,说明高度过高,范围应缩减至较低的一半;如果计算得到的 r 比给定的数值要小,说明高度过低,范围应缩减至较高的一半。

cpp 复制代码
#include <cstdio>
#include <cmath>
const double PI = acos(-1.0);
const double eps = 1e-5;

double f(double R, double h){
    double alpha = 2 * acos(R-h, R);
    double L = 2 * sqrt(R * R - (R-h) * (R-h));
    double S1 = alpha * R * R / 2 - L * (R-h) / 2;
    double S2 = PI * R * R / 2;
    return S1 / S2;
}

double calH(double left, double right, double r){ //r
    double mid;
    while(right - left > eps){
        mid = (left + right) / 2;
        if(f(R, mid) < r){
            left = mid;
        }
        else{
            right = mid;
        }
    return mid;
	}
}

int main(){
    double R, r;
    scanf("%lf%lf", &R, &r);
    printf("%.4f\n", calH(0, R, r)); //保留四位小数
    return 0;
}   
Ⅲ 快速幂

思路一:二分幂 + 递归

cpp 复制代码
//快速幂
//递归边界:b == 0
//递归算式:a * binaryPow(a, b-1, m) % m
//         binaryPow(a, b/2, m) * binaryPow(a, b/2, m) % m

typedef long long LL;
LL binaryPow(LL a, LL b, LL m){
    if(b == 0) return 1;
    else if(b % 2 == 1) return a * binaryPow(a, b-1, m) % m;
    else return binaryPow(a, b/2, m) * binaryPow(a, b/2, m) % m;
}

思路二:二进制 + 迭代

cpp 复制代码
typedef long long LL;
LL binaryPow(LL a, LL b, LL m){
    double ans = 1;
    while(b > 0){
        if(b & 1) ans = ans * a % m; //b的二进制末尾为1
        //why % m
        a = a * a % m;
        b = b >> 1; //b向右移动一位
    }
    return ans;
}

03 Two Pointers

Ⅰ 什么是 two points?

① 给定一个递增的正整数序列和一个正整数 M,求序列中的两个不同位置的数 a 和 b,使得它们的和恰好为 M,输出所有满足条件的方案。例如给定序列 {1, 2, 3, 4, 5, 6} 和正整数 M=8,就存在 2+6=8 与 3+5=8 成立。

cpp 复制代码
//i A[]
//j B[]
//z temp[]

//暴力法
for(int i=0; i<n; i++){
    for(int j=i+1; j<n; j++){
        if(A[i] + A[j] == M){
            printf("%d %d\n", i, j);
        }
    }
}
cpp 复制代码
//two points优化法
while(i < j){
    if(A[i] + A[j] == M){
        i++;
        j--;
    }
    else if(A[i] + A[j] < M){
        i++;
    }
    else{
        j--;
    }
}

② 假设有两个递增序列 A 与 B,要求将它们合并为一个递增序列 C。

cpp 复制代码
int merge(int A[], int B[], int C[], int n, int m){
    int i = 0, j = 0, z = 0;
    while(i < n && j < m){
        if(A[i] <= B[j]){
            C[z++] = A[i++];
        }else{
            C[z++] = B[j++];
        }
    }
    
    while(i < n){
        C[z++] = A[i++];
    }
    while(j < m){
        C[z++] = B[j++];
    }
    return z;
}
Ⅱ 归并排序

思路一:递归实现

cpp 复制代码
const int maxn = 100;
void merge(int A[], int L1, int R1, int L2, int R2){
    int i = L1, j = L2;
    int temp[maxn], z = 0;
    while(i <= R1 && j <= R2){
        if(A[i] <= A[j]){
            temp[z++] = A[i++];
        }else{
            temp[z++] = A[j++];
        }
    }
    
    while(i <= R1){
        temp[z++] = A[i++];
    }
    while(j <= R2){
        temp[z++] = A[j++];
    }
    
    for(i=0; i<z; i++){ //temp为临时数组,数据需要输送回A
        A[L1+i] = temp[i];
    }
}

void mergeSort(int A[], int left, int right){
    int mid;
    if(left < right){ //why if not while
        mid = (left + right) / 2;
        mergeSort(A, left, mid);
        mergeSort(A, mid+1, right);
        merge(A, left, mid, mid+1, right); //why merge end
    }
}

思路二:非递归实现

cpp 复制代码
//i [1, n]
void mergeSort(int A[]){
    for(int step=2; step/2<=n; step*=2){ //step长度
        for(int i=1; i<=n; i+=step){ //step第一个元素L1
            int mid = i + step / 2 - 1;
            if(mid + 1 <= n){
                merge(A, i, mid, mid+1, min(i+step-1, n));
            }
        }
    }
}

//i [0, n-1]
void mergeSort(int A[]){
    for(int step=2; step/2<=n; step*=2){
        for(int i=0; i<n; i+=step){ //改动部分
            int mid = i + step / 2 - 1;
            if(mid + 1 <= n){
                merge(A, i, mid, mid+1, min(i+step-1, n));
            }
        }
    }
}

//sort函数
void mergeSort(int A[]){
    for(int step=2; step/2<=n; step*=2){
        for(int i=1; i<=n; i+=step){
            sort(A+i, A+min(i+step, n+1));
        }
    }
}
Ⅲ 快速排序
cpp 复制代码
//快速排序
//while(left<right) right-- left++
//递归边界:left == right
//递归算式:

int Partition(int A[], int left, int right){
    int temp = A[left];
    while(left < right){
        while(left < right && A[right] >= temp) right--;
        A[left] = A[right];
        while(left < right && A[left] < temp) left++;
        A[right] = A[left];
    }
    A[left] = temp;
    return left;
}

void quickSort(int A[], int left, int right){
    if(left < right){
        int pos = Partition(A, left, right);
        quickSort(A, left, pos - 1); //pos - 1
        quickSort(A, pos + 1, right); //pos + 1
    }
}

拓展:生成随机数

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(){
    srand((unsigned)time(NULL)); //生成随机数种子
    for(int i=0; i<10; i++){
        //rand() % (b-a+1) + a [a, b]
        //(int)((double)rand() % 32767 * (b-a+1) + a) [a, k(b-a)+a]
        printf("%d", rand());
    }
    
    return 0;
}

int Partition(int A[], int left, int right){
    //改动部分
    int p = (int)(round(1.0 * rand() % (right-left) + left));
    swap(A[p], A[left]);
    
    int temp = A[left];
    while(left < right){
        while(left < right && A[right] >= temp) right--;
        A[left] = A[right];
        while(left < right && A[left] < temp) left++;
        A[right] = A[left];
    }
    A[left] = temp;
    return left;
}

04 打表

① 在程序中一次性计算出所有需要用到的结果,之后的查询直接取这些结果。

② 在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果。

③ 对一些感觉不会做的题目,先用暴力程序计算小范围数据的结果,然后找规律,或许就能发现一些 "蛛丝马迹"。

05 递推

对于一个确定位置的 A 来说,以它形成的 PAT 的个数等于它左边 P 的个数乘以它右边 T 的个数。

只需要设定一个数组 leftNumP[ ],记录每一位左边 P 的个数(含当前位,下同),接着从左到右遍历字符串,如果当前位 i 是 P,那么 leftNumP[i] = leftNumP [i-1] +1;如果当前位i不是 P,那么leftNumP[i] = leftNumP[i-1]。

cpp 复制代码
//PAT
//leftNumP[] rightNumP ans%MOD

#include <cstdio>
#include <cstring>
const int MAXN = 100010;
const int MOD = 100000007;;
char str[MAXN];
int leftNumP[MAXN] = {0};

int main(){
    gets(str);
    int len = strlen(str);
    
    for(int i=0; i<len; i++){
        if(i > 0){
            leftNumP[i] = leftNumP[i-1];
        }
        if(str[i] == 'P'){
            leftNumP[i] += 1;
        }
    }
    
    int ans = 0, rightNumT = 0;
    for(int i=len-1; i>=0 ;i--){
        if(str[i] == 'T'){
            rightNumT += 1;
        }
        if(str[i] == 'A'){
            ans = (ans + leftNumP[i] * rightNumT) % MOD;
        }
    }
    
    printf("%d\n", ans);
    return 0;
}

06 随机选择算法

当对 A[left, right] 执行一次 randPartition 函数之后,主元左侧的元素个数就是确定的,且它们都大于主元。假设此时主元是 A[p],那么 A[p] 就是 A[left, right] 中的第 p-left+1 大的数。不妨令 M 表示p-left+1,那么如果 K=M 成立,说明第K大的数就是主元 A[P];如果 K<M 成立,就说明第 K 大的数在主元左侧,即 A[left...(p-1)] 中的第 K 大,往左侧递归即可;如果 K>M 成立,则说明第K大的数在主元右侧,即 A[p+1]...right 中的第 K-M 大,往右侧递归即可。算法以 left==right 作为递归边界,返回A[left]。

cpp 复制代码
//随机选择算法
//递归边界:left == right
//递归算式:K < M randSelect(A, left, p-1, K)
//         K > M randSelect(A, p+1, right, K-M)
//时间复杂度:O(n)

int randSelect(int A[], int left, int right, int K){
    if(left == right){
        return A[P];
    }
    
    int p = partition(A, left, right);
    int M = p-left+1;
    if(K == M){
        return A[p];
    }else if(K < M){
        return randSelect(A, left, p-1, K);
    }else{
        return randSelect(A, p+1, right, K-M);
    }
}

问:给定一个由整数组成的集合,集合中的整数各不相同,现在要将它分为两个子集合,使得这两个子集合的并为原集合、交为空集,同时在两个子集合的元素个数 n1 与 n2 之差的绝对值尽可能小的前提下,要求它们各自的元素之和 S1 与 S2 之差的绝对值尽可能大。求这个绝对值等于多少。

答:这个问题实际上就是求原集合中元素的第 n/2 大,同时根据这个数把集合分为两部分,使得其中一个子集合中的元素都不小于这个数,而另一个子集合内部元素的顺序则不需要关心

相关推荐
垠二42 分钟前
L2-4 寻宝图
数据结构·算法
攻城狮7号3 小时前
【第四节】C++设计模式(创建型模式)-Builder(建造者)模式
c++·设计模式·建造者模式
fpcc3 小时前
设计心得——解耦的实现技术
c++·软件工程
JNU freshman3 小时前
图论 之 迪斯科特拉算法求解最短路径
算法·图论
xinghuitunan4 小时前
时间转换(acwing)c/c++/java/python
java·c语言·c++·python
青松@FasterAI4 小时前
【NLP算法面经】本科双非,头条+腾讯 NLP 详细面经(★附面题整理★)
人工智能·算法·自然语言处理
TechNomad4 小时前
C++访问MySQL数据库
数据库·c++·mysql
旅僧4 小时前
代码随想录-- 第一天图论 --- 岛屿的数量
算法·深度优先·图论
Emplace4 小时前
ABC381E题解
c++·算法