【暖*墟】 #洛谷提高网课# 7.30算法基础

本文详细介绍了算法基础,包括复杂度、时间复杂度、大 O 表示法,以及冒泡排序和归并排序的复杂度。此外,文章深入讲解了C++的STL(标准模板库),如队列、栈、vector、priority_queue、set、multiset、map、pair和二分查找等。还探讨了二维前缀和和高位前缀和的概念。文章通过多个例题进一步巩固了这些算法和数据结构的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

         目录

一. 算法基础

复杂度   运行速度    时间复杂度    大 O

冒泡排序的复杂度  归并排序的复杂度

什么是 STL?  队列 (先进先出)

栈 (后进先出)  vector (不定长数组)

priority_queue (优先队列)

set (去重并已经排序的集合)

multiset (可重集合)

map (散列表&&映射关系)

pair (将2个数据组合成一个数据)

stl中复杂度的比较

auto (自动寻找元素类型)

二分 (logn)  二维前缀和  高位前缀和

二. 例题

1. tyvj 1359 收入计划 (二分答案)

2. 51nod 1105 第 K 大的数

3. 分数规划

4. 纽约

5. 平面最近点对

6. gym100820 Hilbert Sort


一. 算法基础

复杂度

时间复杂度用来衡量程序的运行速度。

运行速度

程序运行一个在内存中寻址、赋值、加法、减法、乘法、除法、开方,

都认为可以在常数的时间内进行,这样的操作都被认为是基本操作。

基于对程序基本操作的说明,一个程序的运行时间不仅跟问题本身有关,

还跟硬件速度、问题规模有关。一般来说,问题的规模越大,

所需要运行的时间就越久;硬件速度越快、程序跑的就越快。

时间复杂度

忽略掉硬件优劣带来的差异,只考虑问题的规模大小对运行时间带来的影响。

以一个程序需要使用的基本操作次数作为衡量程序运行速度的标准:时间复杂度 T(n)。

大 O

对于一个函数 f(n),如果存在 g(n) 和常数 c,当 n 充分大的时候,

始终有 c∗g(n) ≥ f(n),则 g(n) 是 f(n) 的一个渐近上界,写成 f(n) = O(g(n))。

(差不多是到达终点最多可能使用的运行次数)

冒泡排序的复杂度

执行 n 轮,每一轮都会扫描一遍序列并交换相邻逆序对,时间复杂度 T(n) = O(n^2)。

不对?冒泡排序第 i 轮只用扫 n−i 对相邻的数,明明只会执行n(n−1)/2次比较?

在分析复杂度时,我们忽略较低次数的项和最高次项的常数。

归并排序的复杂度

每次分成两半,左右分别排序,递归排序,再将两侧排序好的数组合并。

(分治思想)复杂度 T(n) = 2T(n/2) +O(n) = O(nlogn)。

void Merge(int a[],int left ,int mid,int right){
    int i=left,j=mid+1,n=0,length=right-left; 
//i开始为左半部分最左边,j为右半部分最左边。temp数组是从下标0开始存数。
    while(i<=mid&&j<=right){
        if(a[i]>a[j]){ //左边比右边大。
            temp[n++]=a[j++];
            num+=mid-i+1; //从i到mid都是比a[j]大。
        }
        else temp[n++]=a[i++];     
    }
    if(i>mid){
//因为前面的判断条件是i<=mid,这里说明的是左边全部填满了,那就是填右边了。
        while(j<=right) temp[n++]=a[j++];
    }
    else{
        while(i<=mid) temp[n++]=a[i++];
    }
    for(int k=0;k<=length;k++){ //最后赋值到原数组必须要有的。
        a[left+k]=temp[k];
    }
}

void mergesort(int a[],int left,int right){
    if(left<right){
        int mid=(left+right)/2;
        mergesort(a,left,mid);
        mergesort(a,mid+1,right);
        Merge(a,left,mid,right);
    }
}

什么是 STL?

STL(Standard Template Library)是 C++ 标准模板库,里面提供了大量模板。

队列 (先进先出)

加载库:include < queue >  申明:queue < type > name

queue中元素在内存中不一定连续。

q.push(x) 向队列 q 末尾加入元素 x 。

q.front() 返回队列 q 开头元素。q.back() 返回队列 q 末尾元素。

q.size() 返回队列 q 元素个数。q.empty() 返回队列 q 是否为空。

应用:SPFA算法,BFS。(需要先来先走的情况,扩展节点)

栈 (后进先出)

加载库:include < stack > 申明:stack < type > name

stack中元素在内存中不一定连续。

t.top() 返回 t 栈顶元素。t.pop() 弹出 t 栈顶元素。

t.push(x) 将元素 x 压入 t 栈顶。

vector (不定长数组)

加载库:include < vector > 申明:vector < type > name (从0开始)

vector中元素在内存中连续,支持随机寻址(任意询问、修改任一元素)。

v.push_back(x) 将元素 x 压入 v 末尾。v.clear() 清空 v 中所有元素。

v.begin() 指向 vector 中0号位置的元素的指针地址。

v.end() 指向 vector 中最后一个元素的下一个的指针地址。

应用:题目未知数组长度时,使用 vector 代替数组。

Q:迭代器 it ?(int类型)

A:指向内存中的地址。定义方式 vector<int> ::iterator it;

可以用它去遍历 vector 数组。用‘’*‘’取出指针,*it

it++;//地址向后移一位 ( it+=2 在目前的部分版本中是可以的 )

输出地址指向的数:cout << (*it) << endl;(或 *(it+2) )

priority_queue (优先队列)

加载库:include < priority_queue >  申明:priority_queue < type > name

一般使用:priority_queue< int,vector<int>,greater<int> > q; //小根堆

( 注意有三个元素要写,vector<int>无意义,但要写;或者只写前面的一个也可以 )

重载小于号:

struct node{ //默认大根堆
    int x,y; //先按和排序,再按x排序
    bool operator<(const node &v) const { 
        if(x+y!=v.x+v.y) return x+y < v.x+v.y;
        return x<v.x;
    } //重载之后变为从小到大排序
}; 

priority_queue<node> q;

优先队列就是堆,支持在队列中加入元素、取堆顶、删除堆顶。

q.top() 返回 q 中堆顶。q.pop() 弹出 q 中堆顶。

q.push(x) 将 x 压入 q 中。

set (去重并已经排序的集合)

加载库:include < set >  申明:set < type > name

set支持插入、删除、查找元素,并且支持查询大于(等于)某值的最小元素。

s.insert(x) 将 x 加入集合 s 中。s.begin() 返回集合 s 第一个元素的迭代器。

s.end() 返回集合最后一个元素下一个位置的迭代器。

//↑↑↑已去重集合
for(set<int> ::iterator it=s.begin(); it!=s.end(); it++)
    cout<<(*it)<<endl;

//↓↓↓未去重集合
for(multiset<int> ::iterator it=s.begin(); it!=s.end(); it++)
    cout<<(*it)<<endl;

multiset (可重集合)

加载库:include < multiset >  申明:multiset < type > name

multiset与 set 相同,但允许集合中有多个相同元素。

map (散列表&&映射关系)

加载库:include < map >  申明:map < type1,type2 > name

map是一种关联容器,提供一对一映射处理的能力。

map<int,int> a;

int main(){
    a[2]=5; a[3]=6;
    //pair<int,int> (2,5)
    cout << (*a.find(2)).first << endl;
    cout << (*a.find(2)).second << endl;
    cout << (a.find(2)==a.end()) << endl;
    //a.find(2)返回迭代器所表现的位置
}

pair (将2个数据组合成一个数据)

// 排序时,默认先比较第一关键字,再比较第二关键字。

typedef pair<int,int> mp;
mp b[10]={mp(2,4),mp(3,5),mp(1,5),mp(2,3)};

int main(){
    sort(b,b+4);
    for(int i=0;i<4;i++)
        cout<<b[i].first<<" "<<b[i].second<<endl;
}

stl中复杂度的比较

auto (自动寻找元素类型)

auto it = v.begin(); //用auto赋初始值,自动寻找it类型
for(auto x:v) cout<<x<<endl; //按顺序从前到后输出

二分 (logn)

二分答案的思想:二分答案可能的范围,判断是否可能,寻找最值。

vector / set / multiset 都提供了 lower_bound 函数。

lower_bound(b, e, x) 会返回 [b,e) 中第一个值不小于 x 的地址

upper_bound(b, e, x) 会返回 [b,e) 中第一个值大于 x 的地址。(左闭右开)

#include <algorithm>//必须包含的头文件
#include <stdio.h>
using namespace std;

int main(){
    int n,a[100],m;
    int left,right,i;
    scanf("%d",&n);//设初始数组内元素有n个
    for(i=0;i<n;i++) scanf("%d",&a[i]);
    scanf("%d",&m);//插入的数为m
     
    left = upper_bound(a,a+n,m)-a;//按从小到大,m最多能插入数组a的哪个位置
    right = lower_bound(a,a+n,m)-a;//按从小到大,m最少能插入数组a的哪个位置
 
    printf("m最多能插入数组a的%d\n",left);
    for(i=0;i<left;i++) printf("%d ",a[i]);
    printf("%d ",m);
    for(i=left;i<n;i++) printf("%d ",a[i]);
 
    printf("\n");
 
    printf("m最少能插入数组a的%d\n",right);
    for(i=0;i<right;i++) printf("%d ",a[i]);
    printf("%d ",m);
    for(i=right;i<n;i++) printf("%d ",a[i]);
    return 0;
}

二维前缀和

给定 n∗m 的网格,每个网格中有数值,Q 次询问,给定 (x1,y1) 与 (x2,y2),

求以 (x1,y1) 作为左下角,(x2,y2) 作为右上角形成的子矩形中数值之和。

数据范围 n,m ≤ 300, Q ≤ 10^5。

【分析】

【差分】若已知前缀和 s,求原数组 w 的过程叫做差分

只需要利用 s[i][j] = s[i−1][j] +s[i][j−1]−s[i−1][j−1] +w[i][j]。

移项 w[i][j] = s[i][j]−s[i−1][j]−s[i][j−1] +s[i−1][j−1]。

高位前缀和

【分析】

 

二. 例题

1. tyvj 1359 收入计划 (二分答案)

有长度为 n 的数组,你要将其分成 m 段,使得数组中的每个数都恰好在一段中,

并且使得 m 段中和最大的一段最小,请求出这个最小的值。 数据范围 m≤n≤10^5。

【分析】考虑检验能否将数组分成 m 段使得每一段的和都不超过 x。

从头开始贪心,要超过 x 时切出新的一段 O(n)。

考虑 x 可以时,x+1 也一定可以,即有单调性。二分 x,检验 O(nlogn)。

#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <iostream>
using namespace std;
 
const int N=100005;
int n,m,data[N],l,r,mid,ans,Max,sum;
 
bool check() {
    int cnt=1,sum=0;
    for (int i=1;i<=n;i++){
	sum+=data[i]; //sum是滚动的
        //尽量加在前一组,加不了,再重开一组
	if (sum>mid) cnt++,sum=data[i];
	if (cnt>m) return false;	
    }
    return true;
}
 
int main() {
    cin>>n>>m;
    for (int i=1;i<=n;i++) {
	cin>>data[i];
	Max=max(Max,data[i]);
	sum+=data[i];
    }
    l=Max; r=sum;
    while (l<=r) {
	mid=(l+r)>>1;
	if (check()) ans=mid,r=mid-1;
	else l=mid+1;
    }
    cout<<ans<<endl;
    return 0;
}

2. 51nod 1105 第 K 大的数

给定长度为 n 的数组 A 和 B,将数组 A 和 B 数组中的元素两两相乘,

得到长度为 n∗n 的数组 C,求 C 中第 K 大数。数据范围 n≤ 50000,ai,bi ≤ 10^9。

【分析】

n*log(n)的算法,二分里面再套一个二分。

二分答案,l = a[0]*b[0], r = a[n-1]*b[n-1] 判断 >=mid的数目。

(代码中是求的c中有多少数>=x)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>

using namespace std;
typedef long long LL;
const int MAXN = 5e4+5;
LL a[MAXN], b[MAXN];

LL Judge(LL x, int n){ //找 a[i]*b[j]>=x 的数目
    LL sum = 0, tp;
    for(int i=n-1; i>=0; i--){ //枚举a数组,二分b数组
        if(x % a[i]) tp = x/a[i]+1;
        else tp = x/a[i];
        int tmp = lower_bound(b,b+n,tp)-b;
        sum += n-tmp;
        if(sum == 0) break;
    }
    return sum;
}

int main(){
    int n, k;
    while(~scanf("%d%d",&n,&k)){
        for(int i=0; i<n; i++)
            scanf("%I64d%I64d",&a[i],&b[i]);
        sort(a, a+n); sort(b, b+n);
        LL l = a[0]*b[0], r = a[n-1]*b[n-1];
        while(l <= r){ //二分答案
            LL mid = (l+r)>>1;
            LL tmp = Judge(mid, n);
            if(tmp < k) r = mid-1;
            else l = mid+1;
        }
        printf("%I64d\n",l-1);
    }
    return 0;
}

 3. 分数规划

【分析】

4. 纽约

Azone 决定花费 w 元津巴布韦币,购买一辆载重为 w 的汽车。

共有 n 件家具需要搬运,每件家具的重量为 wi 。

Azone 每次出发前,会搬若干件总重不超过 w 的物品上车:出发前,车是空载的,

Azone 会选择能搬上车的家具中最重的一件放上车(即该家具之前还未运走且放置该家具后汽车不会超载),

然后在剩下的家具中继续选择一件能被搬走的最重的上车,持续装车,直至剩下的家具都塞不上车。

装载完毕后,Azone 会开车运走这些家具,卸在目的地,再驾空车返回继续运送,直至转场完毕。

Azone 希望在运送次数不超过 R的情况下完成转场,求 Azone 最少需要购置价值多少的车。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;

int n,R,a[2003],l,r,mid;
int ans,pre[2003],nxt[2003];

bool okk(int w){
    for(int i=1;i<=n;i++)
        pre[i]=i-1,nxt[i]=i+1; 
    pre[n+1]=n; //链式用于记录仍留下的家具(已按价值排序过)
    for(int i=1,s=0,x=n;s<n;x=pre[n+1],i++){
        if(i>R) return false;
        for(int p=w;p>0 && x;x=pre[x])
            if(p>=a[x]){ //寻找最大可放家具
                s++; p-=a[x];
                nxt[pre[x]]=nxt[x];
                pre[nxt[x]]=pre[x];
            }
    }
    return true;
}

int main(){
    scanf("%d%d",&n,&R);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        r+=a[i],l=max(l,a[i]);
    }
    sort(a+1,a+1+n);
    while(l<r){
        mid=(l+r)>>1;
        if(okk(mid)) r=mid;
        else l=mid+1;
    }
    for(ans=l-50;ans<=l && !okk(ans);ans++);
    printf("%d",ans);
    return 0;
} 

5. 平面最近点对

给定平面上 n 个点,求两两点对之间,欧几里得距离最小的值。

数据范围 n≤ 10^5,xi,yi ≤ 10^5。

【分析】首先将 n 个点按照 x 坐标排序,当 n 个点 x 坐标相同时,扫描一遍, O(n)。

否则我们以 X n/2 做一条竖直的线,将平面切开,左右两边分别只有不超过 n/2 个点,

递归下去求左侧的点两两最短距离 σ1,以及右侧的点两两最短距离 σ2,令 σ = min(σ1,σ2)。

还要考虑左侧的点与右侧的点之间距离的最小值。 显然坐标不在 [xn/2 −σ,xn/2 + σ] 的点不必考虑,

对左侧的一个点 (x,y) 而言, 若存在右侧的点 (a,b) 使得二者距离 < σ,必然有 x−σ < a < x+ σ,

此时 (a,b) 的范围被限定在一个矩形内,最多只有 6 个。如何找到这 6 个点?

只需要找到右侧 a∈ [xn/2 −σ,xn/2 + σ],b∈ [x−σ,x+σ] 的点即可,可以二分查找,

但也可以 two-pointers。 时间复杂度 T(n) = 2T(n/2) +O(n) = O(nlogn)。

6. gym100820 Hilbert Sort

【分析】由于题目保证 S 是奇数,所以每个整点在希尔伯特曲线上都会出现恰好一次。

希尔伯特曲线本身的定义就是递归的,不断将平面分成 4 个小平面,

每个点必定只属于其中一个,递归下去即可。

 

 

                                                ——时间划过风的轨迹,那个少年,还在等你。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值