算法基础(ACWing)

算法基础

基础算法

快速排序

快速排序,背一背板子。

一点心得:

  1. 快速排序不是稳定算法
  2. 快速排序时间复杂度O(nlogn),空间复杂度是O(longn)虽然没有开辟新的空间但是递归占用了栈空间。
  3. 主要的优化就是在排序过程中基准数的选择上,单纯固定取左右端点在面对有序数组时时间复杂度将退化为O(n^2)类似冒泡
  4. 可以使用取中间值法,或者左右端点中间及中间的数三数中去中位数的方法,或者每次使用区间内的的随机数,但是会增大不稳定性
  5. 还有就是,数据量输入比较大的时候用scanf,小的话可以用cin。
#include<iostream>

using namespace std;

const int N =1e6+10;
int q[N];
int n;

//三只取中位数
int mot(int q[],int l,int r){
    int m = l+((r-l)>>1);
    int left = q[l],right = q[r],mid = q[m];
    if(left<mid) swap(left,mid);
    if(left<right) swap(left,right);
    if(mid<right) swap(mid,right);
    return mid;
}
 
void quick_sort(int q[],int l,int r){
    if(l>=r) return;
    int i=l-1,j=r+1;
    //如果下面递归边界用左边界i-1 和i 这里选取基准值p要用l+r+1来保证数据是上取整,否则涉及边界问题
    //同理如果递归使用右边界j与j+1来划分,则此处使用下取整。
    //int p = q[(l+r)>>1];
    
    //三数取中还是右边界j划分不会出错。就先这么背吧。。。
    int p=mot(q,l,r);
    //最终的退出情况就是i==j 所以分点就是i或者j
    while(i<j){
        do i++ ;while(q[i]<p);
        do j-- ;while(q[j]>p);
        if(i<j) swap(q[i],q[j]);
    }
    quick_sort(q,l,j);
    quick_sort(q,j+1,r);
}


int main(){
    cin>>n;
    for(int i=0;i<n;++i){
        scanf("%d",&q[i]);
    }
    
    quick_sort(q,0,n-1);
    
    for(int i=0;i<n;++i){
        printf("%d",q[i]);printf(" ");
    }
}

第k个数

第K个数,类似快排的递归,每次分成两部分之后判断k落在中间数下标的左右哪边,如果落在左边,那么直接递归左边区域重新找k,如果落在右边,那么原区间的第k个就变成右边区间的第k-sl个 sl为这次快排的左侧区间到达分解地点的下标,元素的个数。

#include<iostream>

using namespace std;

const int N = 1e5+10;
int v[N];
int n,k;

int qfk(int l,int r,int k){
    if(l==r) return v[l];
    int p = v[l],i=l-1,j=r+1;
    while(i<j){
        while(v[++i]<p);
        while(v[--j]>p);
        if(i<j) swap(v[i],v[j]);
    }
    int sl = j-l+1;
    if(k<=sl) return qfk(l,j,k);
    return qfk(j+1,r,k-sl);
}


int main(){
    cin>>n>>k;
    for(int i=0;i<n;++i){
        scanf("%d",&v[i]);
    }
    cout<<qfk(0,n-1,k);
    
}

归并排序

  1. 归并排序,时间复杂度是稳定0(nlogn)的 而快排的O(nlogn)是平均时间复杂度。
    因为快排优化的着手点在于每次分界的参照物的选取,我们总是期望可以选到使得递归区间正好分为两半的那个参照物,但并总是可以选到,因此快排的时间复杂度并非总是O(nlogn),以固定取区间端点做参照物的选取方法,在面对基本有序的数组时,时间复杂度将退化到o(n^2)类似冒泡。

  2. 虽然归并排序保证了每次递归区间总是一分为二的,但是相比于快速排序,它需要开辟另外的储存空间来暂存元素,因此它有额外的空间复杂度O(N)。

  3. 令 排序算法中稳定的概念,稳定是指原数列中相同大小的元素排序后相对位置不变。归并排序是稳定的,但快排不是。
    算法的稳定性虽然看上去没什么卵用,但是当进行排列的元素与其他属性进行捆绑之后,元素之间的顺序关系就显得很重要了。

最后,多背板子,熟能生巧。。。

#include<iostream>

using namespace std;

const int N = 100010;
int q[N],tmp[N];

void merge_sort(int q[],int l,int r){
    if(l>=r) return;
    
    int mid = l+r >> 1;
    merge_sort(q,l,mid),merge_sort(q,mid+1,r);
    
    int i = l,j = mid+1,k=0;
    while(i<=mid && j<=r){
        if(q[i] <= q[j]) tmp[k++] = q[i++];
        else tmp[k++] = q[j++];
    }
    while(i<=mid) tmp[k++] = q[i++];
    while(j<=r) tmp[k++] = q[j++];
    for(int i=l,j=0;i<=r;i++,j++) q[i] = tmp[j];
}

int main(){
    int n;
    cin>>n;
    //输入数据记得用取地址符,要不然不知道数字放到哪里去了。scanf操作的是地址上的元素。
    for(int i=0;i<n;++i) scanf("%d",&q[i]);
    merge_sort(q,0,n-1);
    
    for(int i=0;i<n;++i) printf("%d ",q[i]);
    return 0;
}

逆序对的数量

逆序对的数量,结合归并排序的过程来看

  1. 对一个完全无序的数组,想求其中逆序对的数量,需要针对每一个数进行遍历
  2. 在归并排序的过程中,合并mid两边的两个子数组时,mid以左子数组中的元素,在原数组中的位置上 仍然全部位于mid右边子数组中的元素的左边。
  3. 并且左右两边的子数组内元素都是有序排列的。
  4. 在合并的过程中,两个指针i j 分别在两个子数组上游走,当q[i] > q[j]时,去q[i]~q[mid]之间的元素一定也大于q[j] 此时与元素q[j]构成的逆序对数量为 mid - i + 1
  5. 每次移动指针j时 都统计与当前元素q[j]构成逆序对的数量并累加。
  6. 在这次合并完成之后,当前的左右两个子数组合并为同一个数组,在原数组中,这些数组之间的逆序数对的关系已经被找完了,下一次合并时,再作为一个整体统去统计与另外一个区间内元素之间的逆序数对关系,res逐渐累加,最终求完整个数组中的逆序数对,符合分治思想。
  7. 还有就是注意数据量,题目数据量导致res可能会超出int范围,所以使用long long 来接收。
    还是熟能生巧啊,多背多写多理解。。。
#include<iostream>

using namespace std;
typedef long long LL;

const int N = 100010;
int q[N],tmp[N];

LL merge_sort(int l,int r){
    if(l>=r) return 0;
    
    int mid = l+r >> 1;
    LL res = merge_sort(l,mid) + merge_sort(mid+1,r);
    
    int i=l,j=mid+1,k=0;
    while(i <= mid && j<=r){
        if(q[i] <= q[j]) tmp[k++] = q[i++];
        else {
            tmp[k++] = q[j++];
            res += mid - i +1;
        }
    }
    while(i<=mid) tmp[k++] = q[i++];
    while(j<=r) tmp[k++] = q[j++];
    for(int i=l,j=0;i<=r;i++,j++) q[i] = tmp[j];
    return res;
}


int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;++i) scanf("%d",&q[i]);
    
    
    cout<<merge_sort(0,n-1);
    //for(int i=0;i<n;++i) printf("%d ",q[i]);
    return 0;
}

二分

整数二分

数的范围

可以用于整数二分的所有情况 进行二分时,一定要保证剩下的那个区间一定要包含我们需要的情况,这样最后区间长度为1的时候就取到期望值。

当 遇到无解情况时,需要结合题目进行考虑,二分一定是有解的,它总会求出一个答案,至于是否符合题目要求,需要再判。

  • 二分的两种板子,好用的
 //找到数组q中第一个等于target的元素位置,若不存在,则返回的是第一个大于target的元素位置。
    while(l<r){
        int mid = l+r>>1;
        if(q[mid] >= target)  r = mid;
        else l = mid+1;
    }
//找到数组q中最后一个等于target的元素位置
    while(l<r){
        int mid = l+r+1>>1;
        if(q[mid] > target) r = mid-1;
        else l = mid;
    }

灵活运用吧

#include<iostream>

using namespace std;

const int N = 100010;
int m,n,target;
int q[N];

int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;++i){
        scanf("%d",&q[i]);
    }
    while(m--){
        cin>>target;
        int l=0,r=n-1;
        while(l<r){
            int mid = l+r>>1;
            if(q[mid]>=target) r = mid;
            else l = mid+1;
        }
        if(q[l] != target){
            cout<<"-1 -1"<<endl;
            continue;
        }
        cout<<l<<" ";
        l=0,r=n-1;
        while(l<r){
            int mid = l+r+1>>1;
            if(q[mid]>target) r = mid-1;
            else l = mid;
        }
        cout<<l<<endl;
    }
    return 0;
}

浮点数的二分

数的三次方根

数的三次方根,本题是求出一个具体的值。
所以在所有的可能范围区间内,对每一个具体的mid值进行判断,直到区间缩成一点。
第一次接触这种高精度的题,学到了以下几点

  1. 对于非整形(本题中的double)是不能用右移运算符除2的
  2. 判断条件改成while(r-l > 1e-8) 左右区间差值精度最好在题目要求精度再小 两位,不容易截断的时候出错。
  3. cout 在输出时会把无小数点位的小数整合成整数,不符合保留小数的要求。
  4. printf() 函数自动保留小数点后6位输出。
#include<iostream>

using namespace std;

int main(){
    double target;
    cin>>target;
    //cout<<target;
    double l = -10000, r = 10000;
    while(r-l>1e-8){
        double mid = (l+r)/2;
        if(mid*mid*mid <= target) l = mid;
        else r = mid;
    }
    printf("%f\n",l);
    return 0;
}

前缀和与差分

前缀和

前缀和

前缀和可以快速求一段区间内的和
presum[] 数组元素presum[0] = 0
处理数组前缀和时,下标从1开始
原数组元素a0, a1, a2, … a-1 视作 a1, a2, a3, … an
presum[i]表示 原数组从0~i个元素之和
求a1时 用presum[1]-presum[0];
求区间 [l~r] 之间的数字之和时用sum[l-r] presum[r] - presum[l-1]

求取的是数组和是包含区间两个端点的,若用于其他情况,需再考虑用前缀和中的那两个下标去相减。

另,用scanf 与 printf 效率真的不一样。我测试两遍代码
一遍读取全用cin,输出cout,时间1024ms
一遍读取用scanf 输出用printf,时间大概120ms。 又学到了。

#include<iostream>

using namespace std;

const int N = 100010;
int presum[N];

int main(){
    int n,m,t;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){
        scanf("%d",&t);
        presum[i] = presum[i-1] + t;
    }
    
    while(m--){
        int l,r;
        scanf("%d%d",&l,&r);
        printf("%d\n",presum[r]-presum[l-1]);
    }
    return 0;
}

二维前缀和

子矩阵的和

二维数组前缀和

快速求取一个矩阵中的某个小矩阵内的元素和,可以用到二维数组的前缀和。
首先构建二维数组,尽量下标从1开始,这样可以与前缀和中的数字下标保持一致。
构建二维前缀和数组 公式
这里减去presum[i-1][j-1]因为减去两个区域和的重叠部分,最后补上a[i][j] 得到完整从区域和 (i,j)到所有左上角元素的元素之和。
presum[i][j] = presum[i-1][j] + presum[i][j-1] - presum[i-1][j-1] + a[i][j]

求区间左上角坐标(x1,y1) 右下角坐标(x2,y2)之间的所有元素和。
当处理好数组的二维数组前缀和矩阵后,用公式
ans = presum[x2][y2] - presum[x2][y1-1] - presum [x2-1][y1] + presum[x1-1][y1-1]
补上的部分是做减法时被多减去的部分。

画个图就非常清楚了~

然后就是要注意,前缀和数组的下标都是从1开始的,为了方便,把原数组的元素也从1开始是最好的。

#include<iostream>

using namespace std;

const int N = 1010;
int n,m,q;

int a[N][N];
long long presum[N][N];

int main(){
    int n,m,q;
    scanf("%d%d%d",&n,&m,&q);
    for(int i=1;i<=n;++i){
        for(int j=1;j<=m;++j){
            scanf("%d",&a[i][j]);
        }
    }
    
    //初始化前缀和表
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
         presum[i][j] = presum[i-1][j] + presum[i][j-1] - presum[i-1][j-1] + a[i][j];   
        }
    }
    while(q--){
        int li,lj,ri,rj;
        scanf("%d%d%d%d",&li,&lj,&ri,&rj);
        printf("%d\n",presum[ri][rj] - presum[li-1][rj] - presum[ri][lj-1] + presum[li-1][lj-1]);
    }
    return 0;
    
}

双指针

判断子序列

双指针判断一个序列是否是另一个序列的子序列

双指针i维护a[]数组中的元素,j维护数组b[]中的元素,以主串遍历为条件进入循环,若a序列中元素与b序列中元素相等,则i++ 若i走到数组a最后一个元素 即 i == n-1 说明当前a已经可以确定成为b数组中的一个子序列。

最重要的是分清 子序列子串
子序列不要求连续,顺序相同即可,子串要求必须连续,一旦匹配到不同的元素,则模式串中的i需要从头开始。
这样看来判断子序列还是要更简单一些。

#include<iostream>

using namespace std;

const int N = 100010;
int a[N],b[N];

int main(){
    int n,m;
    cin>>n>>m;
    for(int i=0;i<n;++i) scanf("%d",&a[i]);
    for(int i=0;i<m;++i) scanf("%d",&b[i]);
    int i=0,j=0;
    while(j<m){
        if(i == n-1 ){
            cout<<"Yes";
            return 0;
        }
        if(a[i] == b[j]) i++;
        j++;
    }
    cout<<"No";
    return 0;
    
}

位运算

二进制中1的个数

方案一 int型共32位,每次统计最后一位和1按位与 若结果为1则当前数字内有一个1 最多移动32次,找到这个数字内的所有的1;
(本题局限之一,本题所有数值都非负,所以移位操作可以拉满32位,所以本方法可以AC)

#include<iostream>

using namespace std;

const int N = 100010;
int q[N];

int main(){
    int n = 0;
    scanf("%d",&n);
    for(int i=0;i<n;++i) scanf("%d",&q[i]);
    for(int i=0;i<n;++i){
        int t = 0;
        int T = 32;
        while(T--){
            t += q[i]&1;
            q[i] = q[i]>>1;
        }
        q[i] = t;
    }
    for(int i=0;i<n;++i) printf("%d ",q[i]);
    return 0;
}

方案二
lowbit() 实现,lowbit可以返回当前数字的二进制表示最后一个1与它的所有低位0组成的数字,每次减去直到该数为0,操作次数就是当前数字内含1的个数
lowbit()记得自行定义,它的原理就是计算机中用补码来表示一个数字的负数

#include<iostream>
#include<string>

using namespace std;

const int N = 100010;
int q[N];
int lowbit(int x){
    return x & -x;
}

int main(){
    int n = 0;
    scanf("%d",&n);
    for(int i=0;i<n;++i) scanf("%d",&q[i]);
    for(int i=0;i<n;++i){
        int temp = 0;
        while(q[i]){
            q[i] -= lowbit(q[i]);
            temp++;
        }
        q[i] = temp;
    }
    for(int i=0;i<n;++i) printf("%d ",q[i]);
    return 0;
}

离散化——区间和

区间和

感觉有点复杂的一道题。
题目特点是给定的元素取值范围(值域)很大 在1e9范围,但是实际上 个数(1e5范围) 远远小于 范围。所以如果直接用数组前缀和,会有很多浪费,无论是操作还是数组空间都造成浪费。
所以可以用保序的离散化映射,按顺序将他们映射到1e5的位置上。称之为离散化。

具体方法是
将所有给定的数字与查询位置存放在vec中,排序,并去重。
对于每一个要求和的元素x,在vec中找到它的下标 使用二分法找到数组vec中第一个大于等于x的位置,并返回它在vec数组中的下标。
注意他的下标在vec中是从0开始 到vec.size()-1的。
但我们需要另数组a从下标1开始,所以返回值的时候要取+1操作。这样让这些元素在数组a[]中映射下标为1-vec.size()

求出离散化的数组a[]后,对其进行前缀和预处理。得到数组s[]
注意这里的a s下标都是经过离散化处理的,所以在对querry数组中的两个下标进行请求时,也要首先对两个下标进行离散化处理,再在前缀和数组s中用离散化处理后的边界求解。

要注意的问题

  1. 数组中可能存在重复元素,所以需要在求离散化后位置的元素去重,保证每次离散化请求得到的数组下标是唯一的,可以保证元素是被加到正确位置上的。
  2. 去重采用c++的vector容器及unique()库函数。
  3. unique()库函数会对元素内容器进行去重,并将所有重复的元素放在数组的最方,并返回最后面重复元素的第一个位置的迭代器,删除从这个迭代器位置到该容器最后的所有元素完成对vector内元素的去重。
#include<iostream>
#include<vector>
#include<algorithm>

using namespace std;

const int N = 300010;

int a[N],s[N];

vector<int> vec;
vector<pair<int,int>> add,querry;

int find(int x){
    int l=0,r=vec.size()-1;
    while(l<r){
        int mid = l+r >> 1;
        if(vec[mid] >= x) r = mid;
        else l = mid+1;
    }
    return r+1;
}

int main(){
    int n,m;
    cin>>n>>m;
    
    while(n--){
        int p,v;
        scanf("%d%d",&p,&v);
        add.push_back({p,v});
        vec.push_back(p);
    }
    while(m--){
        //querry
        int l,r;
        scanf("%d%d",&l,&r);
        querry.push_back({l,r});
        vec.push_back(l);
        vec.push_back(r);
    }
    
    //去重
    sort(vec.begin(),vec.end());
    vec.erase(unique(vec.begin(),vec.end()),vec.end());
    
    //查找离散化以后数组下标的find函数,并插入
    for(auto ad:add){
        a[find(ad.first)] += ad.second; 
    }
    //对于a[]数组,预处理前缀和数组s[];
    
    for(int i=1;i<=vec.size();++i){
        s[i] = s[i-1] + a[i];
    }
    
    //开始询问 对于返回所求值
    for(auto q:querry){
        int l = find(q.first),r=find(q.second);
        printf("%d\n",s[r] - s[l-1]);
    }
    return 0;
    
}

双指针

双指针

双指针的最大作用就是可以用于算法的优化,双指针的复杂度应当是O(N)的,往往是将复杂度从O(N^2)优化成O(N)。

  • 模板
//双指针算法 (最长区间问题)
for(int i=0,j=0;j<n;++i){
	while( i<=j && check(i,j)) j++;
    res = max(res,j-i+1);
}

最大收获,双指针的主要作用是对复杂度O(N^2)的方法进行优化,往往可以优化成为O(N)
和y总的不太一样,我习惯用j做先行指针,因此每次找到的不重复连续子序列长度为 j-i+1。

本题,小数据量情况下,用来判断已选区间内是否有重复的方法为 用另一个数组来统计先行探针j扫过的数据,对其+1。每次探针j右移一步,首先判断S数组中当前元素是否已经存在,若已存在(S[a[j]] > 1)则说明遇到了重复,说明这个连续子区间已经被更新完毕,此时需要右移左指针i。为了不影响下一轮搜索过程中对重复元素的判断,i有一过程中,将之前因为j指针被添加的s[a[j]]元素清零(减一操作)。

#include<iostream>

using namespace std;

const int N = 100010;
int a[N],s[N];
int n;
 int main(){
     int res;
     cin>>n;
     for(int i=0;i<n;++i) scanf("%d",&a[i]);
     
     for(int i=0,j=0;j<n;j++){
         s[a[j]]++;
         while(s[a[j]] > 1){
             s[a[i]]--;
             i++;
         }
         res = std::max(res,j-i+1);
         }  
         cout<<res;
     return 0;
 }

本题判断元素重复的时候,s[a[i]]的元素组成仅为1或0,所以或许s[]数组可以设计成存储bool量的。或者采用hash表来做。若用bool数组来表示s[] 需要把更新s[a[j]]的逻辑后放,每次更新i~j区间长度之后再对其置true。就是有一点点小小的区别。

#include<iostream>

using namespace std;

const int N = 100010;
int a[N];
bool s[N];
int n;
 int main(){
     int res;
     cin>>n;
     for(int i=0;i<n;++i) scanf("%d",&a[i]);
     
     for(int i=0,j=0;j<n;j++){
         
         while(s[a[j]]== true){
             s[a[i]] = false;
             i++;
         }
         res = std::max(res,j-i+1);
         s[a[j]] = true;
         }  
         cout<<res;
     return 0;
 }

区间合并

区间合并

  • 区间合并!

    • 区间合并套路
    1. 首先读入所有的区间,起始点用pair<int,int>形式存起来(最好是放在vector中,可以用函数sort)
    2. 然后对左端点进行排序 vector的话直接用sort就行,记得头文件包含 和
    3. 维护一个区间[s,e] 对于排序完成的每一个序列对aa,进行判断。
    4. 若aa的起点严格大于e 即当前维护的区间末端与当前aa区间不交接,判断:
    5. 若此时维护区间左端点为负无穷(即还没被初始更新过)就进行初始化更新,将s更新为aa区间起点aa.first;

    6. 若此时维护区间左端点并非负无穷(即当前正在维护一个有效区间) 说明已经当前维护区间已经是一个有效的区间,将{s,e}加入答案vector容器res,并将维护区间左端点s更新为当前区间左端点aa.first

    7. 若当前处理区间aa起点aa.first并非严格大于维护区间终点e,说明当前区间aa左端点被包含在[s,e]中,此时判断:
    8. 若aa终点严格大于[s,e]终点e 说明当前维护区间可以被合并为更大区间,将e更新为aa.second

    9. 否则,说明当前aa被包含在维护区间[s,e]之中,不会对当前区间做什么贡献。

    10. 退出循环时,要注意,上述逻辑会忽略掉对最后一段有效区间的处理,判断:
    11. 若此时维护的区间未被初始化,说明待判断的区间数组为空,不更新答案。

    12. 否则,将最后维护的区间[s,e]加入答案。

    13. 完成

    单指针维护区间写法

    #include<iostream>
    #include<vector>
    #include<algorithm>
    
    using namespace std;
    
    const int N = 100010;
    int n;
    
    typedef pair<int,int> PII;
    vector<PII> a;
    
    void merge(vector<PII>& a){
        int s = -2e9,e = -2e9;
        vector<PII> res;
        sort(a.begin(),a.end());
        for(auto aa:a){
            if(e < aa.first){
                if(s != -2e9 ) res.push_back({s,e});
                s = aa.first,e = aa.second;
            }
            else e = std::max(e,aa.second);
        }
        if(s != -2e9) res.push_back({s,e});
        a = res;
    }
    
    int main(){
    
        cin>>n;
        for(int i=0;i<n;++i) {
            int l,r;
            scanf("%d%d",&l,&r);
            a.push_back({l,r});
        }
        
        merge(a);
        
        cout<<a.size();
        return 0;
    }
    

    另一个双指针,两个数组分别维护起始点的写法。

    1. 将所有区间的起点,终点分别存放在两个vector中再分别LRRR进行排序,
      对于起始点数组LR与区间终点数组RR;
    2. 定义i指针在LR中移动,j在RR上移动。考虑RR[j] 与 LR[j+1]的关系,若RR[j] >= LR[j+1] 说明当前起点LR[i]对应的最近一个终点内包含另一端区间起点LR[j+1],遂合并,判断次接近终点RR[j+1]与LR[j+1+1]的关系,
    3. 以此类推,直到j走到终点n-1 或者某个右边端点内不再包含任意区间起点。
    4. i更新为j+1,j更新为i。
    5. 将结果压入数组。
    6. 完成。
    #include<iostream>
    #include<vector>
    #include<algorithm>
    
    using namespace std;
    vector<int> LR,RR;
    vector<pair<int,int>> res;
    int n;
    
    void merge(vector<int>& LR,vector<int>& RR){
        sort(LR.begin(),LR.end());
        sort(RR.begin(),RR.end());
        for(int i=0,j=0;i<n;){
            while(j<n-1 && RR[j] >= LR[j+1]) j++;
            res.push_back({LR[i],RR[j]});
            j++,i = j;
        }
        return;
    }
    
    int main(){
        cin>>n;
        for(int i=0;i<n;i++){
            int l,r;
            scanf("%d%d",&l,&r);
            LR.push_back(l),RR.push_back(r);
        }
        
        merge(LR,RR);
        cout<<res.size();
        return 0;
        
    }
    

数据结构基础

链表

单链表

在笔试过程中,使用单链表,若数据量太大,使用动态链表时,频繁地使用new操作非常容易超时。因此可以考虑使用静态链表,通过数组来实现。
链表组成

  1. e[x]数组用来存储链表中编号x节点中元素的值 value
  2. ne[x]数组用来存储链表中编号x节点元素的next指针,即x节点的下一位节点编号为ne[x].
  3. head表示头结点的下标
  4. idx表示 索引 用来存储当前操作到的某一个位置 从第一个点开始,每当要分配一个新地址的时候,就把idx指向的地址赋给它,idx自行后移一位。

链表操作

  • 链表的初始化
void init(){
     head = -1;
     idx = 0;
}
  • 链表插到头节点的位置
void add_to_head(int x){
    e[idx] = x;
    ne[idx] = ne[head];
    head = idx;
    idx++;
}
  • 链表删除编号为k的节点
void remove(int k){
    ne[k] = ne[ne[k]];
}
  • 在任意位置k节点后插入一个节点
void add(int k,int x){
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}

基础操作完成
本题注意k的起始位置是1,所以传参数的时候注意k-1

#include<iostream>

using namespace std;

const int N = 100010;

int e[N],ne[N];
int head,idx;

void init(){
    head = -1;
    idx = 0;
}

//头插法插入一个节点
void add_to_head(int x){
    e[idx] = x;
    ne[idx] = head;
    head = idx;
    idx++;
}
//x插入到下标为k点的 后面 
void add(int k,int x){
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx;
    idx++;
}
//链表删除下标为k的节点后面的点
void remove(int k){
    ne[k] = ne[ne[k]];
}

int main(){
    init();
    int m,op1,op2;
    char op;
    cin>>m;
    while(m--){
        
        cin>>op;
        
        if(op == 'H'){
            scanf("%d",&op1);
            add_to_head(op1);
        }
        else if(op == 'I'){
            scanf("%d%d",&op1,&op2);
            add(op1-1,op2);
        }
        else if(op == 'D'){
            scanf("%d",&op1);
            if(op1 == 0) head = ne[head];
            remove(op1-1);
        }
    }
    for(int i=head;i != -1;i = ne[i]) cout<<e[i]<<" ";
    return 0;
}

双链表

双链表

双链表,类似于单链表,不过需要维护一头一尾,然后要用两个数组L[] 与 R[]分别来记录每个节点的左右指针。
类比单链表,还是很好理解的。不多写了。

这里要注意,初始化的时候,我们已经把 头节点 尾节点的编号 0 1占用了,所以在进行对编号为k的节点操作时,需要令
k = k + 1
这是因为0 1 被占用,所以虽然我们插入节点时编号从1开始,但实际上在数组中的下标是从2开始的。因此需要+1操作才能找到数组中存放的正确节点。

#include<iostream>

using namespace std;

const int N = 100010;

int e[N],L[N],R[N],idx;

void init(){
    //初始化,头尾端点首先互相指向。完成初始化。0表示左端点,1表示右端点。
    L[1] = 0;
    R[0] = 1;
    idx = 2;
}

/*radd 注意指针指向顺序:
    右边插入,先将新节点的值存入e[]数组;
    然后将新节点的左右指针指向正确位置;
    然后另k节点原来的右边节点的左指针指向新节点;
    最后是k节点的右指针指向新节点;
*/
void radd(int k,int x){
    e[idx] = x;
    L[idx] = k;
    R[idx] = R[k];
    L[R[k]] = idx;
    R[k] = idx;
    idx++;
}
//同理 k的左边插入节点,或者传入参数 L[k] ,千万注意不是 k-1 k作为编号,在链表中的位置不一定是连续的。
void ladd(int k,int x){
    e[idx] = x;
    L[idx] = L[k];
    R[idx] = k;
    R[L[k]] = idx;
    L[k] = idx;
    idx++;
}

void remove(int k){
    R[L[k]] = R[k];
    L[R[k]] = L[k];
}

int main(){
    int m,k,x;
    string op;
    init();
    cin>>m;
    while(m--){
        cin>>op;
        if(op == "L"){
            cin>>x;
            radd(0,x);
        }
        else if(op == "R"){
            cin>>x;
            radd(L[1],x);
        }
        else if(op == "D"){
            cin>>k;
            remove(k+1);
        }
        else if(op == "IL"){
            cin>>k>>x;
            radd(L[k+1],x);
        }
        else{
            cin>>k>>x;
            radd(k+1,x);
        }
    }
    
    for(int i=R[0]; i!= 1; i = R[i]) cout<<e[i]<<" ";
    
    return 0;
}

模拟栈

数组模拟栈 用游标 tt 表示栈顶元素位置。

栈只关注栈顶元素,所以很方便。

#include<iostream>

using namespace std;

const int N = 100010;

int stk[N],tt;

void init(){
    tt = 0;
}

void push(int x){
    stk[++tt] = x;
}
void pop(){
    stk[--tt];
}
string empty(){
    return tt == 0 ? "YES" : "NO"; 
}
int querry(){
    return stk[tt];
}

int main(){
    int m,x;
    string op;
    cin>>m;
    while(m--){
        cin>>op;
        if(op == "push"){
            cin>>x;
            push(x);
        }
        else if(op == "pop"){
            pop();
        }
        else if(op == "empty"){
            cout<<empty()<<endl;
        }
        else if(op == "query"){
            cout<<querry()<<endl;
        }
    }
    return 0;
}

表达式求值

表达式求值

对于一个中缀表达式,叶子节点存数,子节点存运算符。如果一棵子树被遍历完毕,那么这颗子树的表达式就可以求出来了,那么如何判断某棵子树被遍历完了?

只需判断当前运算符的优先级是否>=上一个字符的优先级(在不考虑括号的情况下)
如果考虑括号呢?
需要对括号单独考虑,当遇到一个’)‘就从右向左结合操作符直到遇到上一个匹配的’('为止。

这是模板题啊,可以背,可以扩展。
如果出现运算优先级更高的运算符,可以扩展哈希表,然后补全特殊操作。

对于表达式树,所有的内部节点为 运算符,所有的叶节点是运算的数。

对于中缀表达式,任何一颗子树被遍历完毕,该子树的值就可以被用作下一步操作。

#include<iostream>
#include<algorithm>
#include<stack>
#include<unordered_map>
#include<string>

using namespace std;
stack<int> num;
stack<char> op;

void eval(){
    int b = num.top();num.pop();
    int a = num.top();num.pop();
    auto c = op.top();op.pop();
    int x = 0;
    if( c == '+' ) x = a + b;
    else if( c == '-' ) x = a-b;
    else if( c == '*' ) x = a*b;
    else if( c == '/' ) x = a/b;
    num.push(x);
}

int main(){
    unordered_map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}};
    string str;
    cin>>str;
    for(int i=0;i<str.size();++i){
        char c = str[i];
        if(isdigit(c)){
            int x = 0,j = i;
            while(j<str.size() && isdigit(str[j])){
                x = x*10 + str[j++] - '0';
                i = j-1;
            }
                
            num.push(x);
        }
        else if(c == '(') op.push(c);
        else if(c == ')') {
            while(op.top()!= '(') eval();
            op.pop();
        }
        else{
            while(op.size() && pr[op.top()] >= pr[c]) eval();
            op.push(c);
        }
    }
    while(op.size()) eval();
    cout<<num.top();
}

模拟队列

模拟队列

数组模拟队列,没啥好说的。
开辟数组,使用两个游标 i j 分别表示队列的头元素下标与待插入元素的下标。
入队操作,在j位置插入数据,然后j后移一位。
出队操作,队头元素右移一位即可。
判空,判断ij是否相遇,也就是 下一个插入的位置是队头嘛?如果是,那么当前队列就是空的呗。
查询,返回队列第一个元素,就是队头元素,返回q[i]即可。

但是如果出栈操作太过频繁,可能会导致数组位置不够用,加入一个初始化操作,一旦出栈导致队空,将i j 同时移到0
清空数组。

因为我们实际实现的队列是i-j-1之间的元素,所以哪怕这个数组原来的位置上有数字也不影响,因为每次出出队的元素一定是在上此栈空以后新入对的,在执行入队操作时这些元素都会被更新,所以不会收到数组中原位置上的元素影响。

#include<iostream>
#include<string>

using namespace std;

const int N = 100010;

int q[N],i,j,m,x;
string op;

void init(){
    i = 0,j = 0;
}

void push(int x){
    q[j++] = x;
}
void pop(){
    i++;
    if(i == j) init();
}
string empty(){
    return i == j ? "YES" : "NO";
}
int query(){
    return q[i];
}

int main(){
    cin>>m;
    init();
    while(m--){
        cin>>op;
        if(op == "push"){
            cin>>x;
            push(x);
        }
        else if(op == "pop"){
            pop();
        }
        else if(op == "query"){
            cout << query()<<endl;;
        }
        else cout<<empty()<<endl;;
    }
    
    return 0;
}

单调栈

单调栈

单调栈,用处不是很广泛,可以用于快速找出一个序列中 某个元素的最近一个更大或者更小的元素

数组实现单调栈st[]
遍历每一个元素,栈内元素是所有比当前元素位置更靠左的元素,栈顶是最后一个入栈的元素,也就是离当前元素位置最近的元素。进行比较。

若栈顶元素大于等于当前元素,则栈顶元素不符合题意,出栈即可。
这里,当前栈顶元素出栈以后,哪怕接下来的一个元素比当前出栈的元素大,那么它也一定会比当前比较的元素大,而当参与比较的元素位置一定更接近,所以这个栈顶元素出栈也不影响后续操作。但是当前比较元素最后一定要入栈。

检查每一个元素与栈顶元素,若栈为空,则当前元素的左边没有比它更小的元素,输出 -1
若栈不为空,则当前栈顶元素就是最近一个比它更小的元素。

最后将该元素压入栈。保证栈内的元素是每一个来判断检查的元素的左侧更小元素。

#include<iostream>

using namespace std;

const int N = 100010;

int st[N],tt,n;

int main(){
    cin>>n;
    int x;
    for(int i=0;i<n;++i){
        cin>>x;
        while(tt && st[tt] >= x) tt--;
        if(tt) cout<<st[tt]<<' ';
        else cout<< -1 << ' ';
        
        st[++tt] = x;
    }
    
    return 0;
}

单调队列

滑动窗口求每个窗口内的最大最小值

#include<iostream>

using namespace std;

const int N = 1000010;

int n,k,a[N],q[N];

int main(){
    scanf("%d%d",&n,&k);
for(int i=0;i<n; ++i) scanf("%d",&a[i]);

//寻找每个窗口的最小值
int hh = 0,tt = 0;//初始化队列的头尾两个指针
for(int i=0;i<n;++i){
    if( hh <= tt && q[hh] < i - k + 1 ) hh++;//如果当前队头 出了窗口,就及时删掉它。
    while(hh <= tt && a[q[tt]] >= a[i]) tt--;
    q[++tt] = i;
    
    //保证队中有k个元素,才执行第一次输出,此时是单调增队列,所以队头是滑窗内最小值。
    if(i >= k-1) printf("%d " , a[q[hh]]);
}

//cout<<endl;
puts("");

//寻找窗口内的最大值
hh = 0,tt = 0;
for(int i=0;i<n;++i){
    if( hh <= tt && q[hh] < i - k + 1 ) hh++;
    while(hh <= tt && a[q[tt]] <= a[i]) tt--;
    q[++tt] = i;
    
    if(i >= k-1) printf("%d " , a[q[hh]]);
}
//puts("");

return 0;
}

KMP

KMP字符串

KMP,学了又忘得,索性背模板得了。

预处理next数组的过程和匹配过程核心都是 回退

Y总这个板子字符数组有效字符下标都是从1开始的,可以避免越界问题

ne[j]表示,当匹配串s[i]与我当前的模式串p[j+1]不能匹配的时候,j需要回退到ne[j]的位置,此时的p[j+1]是有可能与当前的s[i]相等的。判断是否相等,如果相等,则j前进一位。若此时 j==n 说明模式串已经被遍历完毕,说明完成了匹配。

处理next数组的时候 j表示当前最长相等前后缀的最后一位,随着i向后遍历,如果p[j+1]!=p[i]说明此时最长相等前后缀不会增加,所以j需要回退,找到与当前i匹配的上一个最长相等前后缀的尾部 j = ne[j];若p[j+1] = p[i]则此时以j为最长相等前后缀又可以增加1 此时j++;更新ne[i] = j,表示到p[i]为止的这部分子串的最长相等前后缀的长度。

#include<iostream>

using namespace std;

const int N  = 100010 , M = 1000010;

char p[N],s[M];
int ne[N];
int n,m;

int main(){
    //注意此处的字符串数组s p 是从原地址后一位开始的,有效字符从s[1]和p[1]开始。
    cin>>n>>p+1>>m>>s+1;
    
    //预处理ne[]
    for(int i=2,j=0;i<=n;++i){
        while(j && p[i]!=p[j+1]) j = ne[j];//不相等就回退
        if(p[i] == p[j+1]) j++;//回退以后p[i]与p[j+1]相等,则最长相等前后缀+1并更新给p[i]否则,直接更新给p[i]
        ne[i] = j;
    }

    //kmp匹配的过程,i在匹配串,j在模式串
    for(int i=1,j=0;i<=m;++i){
        while(j && s[i]!=p[j+1]) j = ne[j];//此时模式串s[i]与p[j]不能匹配,j不能再前进了,只能选择回退j
        if(s[i] == p[j+1]) j++;
        if(j == n){
            //j走到模式串最后一位,此时匹配成功
            printf("%d ",i-n);
            j = ne[j];
        }
    }
    
    return 0;
}

Trie树

Trie树字符串统计

Trie树,第一次接触。
是可以高效存储和查找字符串集合的数据结构。
主要应用在组成元素种类不太多的情况下,可以做到对字符串的快速插入和查询。
可以通过二维数组来模拟实现。
每一个单词的最后结尾部分,要加一个标记,用来标志一个字符串的结束。

#include<iostream>

using namespace std;

const int N = 100010;

//题目中保证全部都是小写英文字母,每个节点最多伸出26个分支
int son[N][26] , cnt[N],idx;//下标是0的点,既是根节点,又是空节点。
char str[N];

void insert(char str[]){
    //p表示当前节点
    int p = 0;
    //遍历待插入字符数组str[] 若树中节点son[p][u]不存在,则构建新节点som[p][u],并更新当前节点p。
    for(int i=0;str[i];i++){
        int u = str[i] - 'a';
        if(!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
    cnt[p]++;
}

int query(char str[]){
    int p=0;
    for(int i=0;str[i];i++){
        int u=str[i]-'a';
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

int main(){
    int n;
    cin>>n;
    char op[2];
    while(n--){
        //读取字符数组,不加取地址符,数组名将自动被解释称第一个字符的地址。
        scanf("%s%s",op,str);
        if(op[0] == 'I') insert(str);
        else printf("%d\n",query(str));
    }
    return 0;
}

最大异或对

求取最大异或对的值,对每个int元素的31位建树;
查找最大异或对时,每次去找与原来元素相同位置上相异的走法(你原来这里是0我就走1,直到走无可走再走);
每当成功找到一个与原来位置不同的走法,这次遍历的异或值将+1,最后统计最大的那个值。

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 100010,M = 3000000;
int trie[M][2],nums[N],idx;
int m;

void insert(int x){
    int p = 0;
    for(int i=30;i>=0;--i){
        int &t = trie[p][x>>i&1];//因为下面要修改trie[p][x>>i&1]的值了,但是太长写起来很麻烦所以用引用的方式,取个别名t 否则无法修改到trie[p][x>>i&1]的值
        if(!t) t = ++idx;
        p = t;
    }
}

int query(int x){
    int res = 0,p = 0;
    for(int i=30;i>=0;--i){
        int t = x>>i&1;
        if(trie[p][!t]){
            res += 1 << i;
            p = trie[p][!t];
        }
        else{
            p = trie[p][t];
        }
    }
    return res;
}

int main(){
    cin>>m;
    for(int i=0;i<m;++i){
        scanf("%d",&nums[i]);
        insert(nums[i]);
    } 
    int res = 0;
    for(int i=0;i<m;++i){
        res = std::max(query(nums[i]),res);
    }
    cout<<res<<endl;
    
    return 0;
}

并查集

合并集合

并查集,主要具备两个功能:

  1. 将两个集合合并
  2. 询问两个元素是否在一个集合当中

基本原理
每个集合,用一棵树来表示。树根的编号就是整个集合的编号。数中的每个节点存储它的父节点,p[x]表示x的父节点。

三个核心问题

  1. 如何判断树根? p[x] == x 只有树根的值与是它本身(集合编号)
  2. 如何求x的集合编号? while(p[x]!=x) x = p[x];一直遍历上去
  3. 如何合并两个集合? p[x] = y,直接两树并作一棵

优化

  1. 路径压缩(常用)
  2. 按值合并
#include<iostream>

using namespace std;

const int N = 100010;

int p[N];
int n,m;
//返回x的祖宗
int find(int x){
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}


int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<n;++i) p[i] = i;
    
    char op;
    int x,y;
    
    while(m--){
        scanf("%s%d%d",&op,&x,&y);
        //合并过程,把一棵树的祖宗直接换成另一颗树的祖宗(换祖宗)
        if(op == 'M' ) p[find(x)] = find(y);
        
        else {
            if(find(x) == find(y)) printf("Yes\n");
            else printf("No\n");
        }
    }
    
    return 0;
}

连通块中点的数量
与并查集上一道题几乎一致,只是多了一个需要记录的每个集合个数,用另一个数组来记录每个根节点下子孙的数量集合;
在合并的时候更新这个数组。

#include<iostream>

using namespace std;

const int N = 100010;
int p[N],cnt[N],n,m;

int find(int x){
    while(p[x] != x) x = p[x];
    return p[x];
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;++i){
        p[i] = i;
        cnt[i] = 1;
    }
    char op[3];
    int x,y;
    while(m--){
        scanf("%s",op);
        if(op[1] == '1'){
            scanf("%d%d",&x,&y);
            if(find(x) == find(y)) puts("Yes");
            else puts("No");
        }
        else if(op[1] == '2'){
            scanf("%d",&x);
            printf("%d\n",cnt[find(x)]);
        }
        else{
            scanf("%d%d",&x,&y);
            //更新维护集合的数组。
            if(find(x) == find(y)) continue;
            //顺序不要搞错了,先把被并入的集合元素加到新的根节点上,然后将被并入的根节点指向新的根节点。
            cnt[find(x)] += cnt[find(y)];
            p[find(y)] = find(x);
        }
    }
    return 0;
}

食物链
食物链
并查集可以同时维护其他的额外信息。

  1. 本题只有三个元素,记录每个节点与根节点之间的关系,三种关系,用距离模3处理
  2. 余1的节点可以吃根节点;余1的节点可以被根节点吃;余0的点与根节点同类,它可以吃与被根节点吃的,也会被吃根节点的节点吃
  3. 距离是什么?类似代的概念,根节点是第0代,第1代吃第0代,第2代吃第1代;类推。

说实话,这个题感觉有点难以理解。。。有点一知半解的感觉,过段时间再回来看看吧。
p[]数组维护节点的父节点,d[]数组维护每个节点到根节点的“距离”
三种情况映射三种距离关系,所以对距离取模3

下面的逻辑看清楚了,更新父节点的find()逻辑那里还不是很懂,递归和迭代一直用的不太好。。。

#include<iostream>

using namespace std;

const int N = 50010;
int p[N],d[N];
int n,m;

int find(int x){
    if(p[x] != x){
        int t = find(p[x]);
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i) p[i] = i;
    
    int res = 0;
    int op,x,y;
    while(m--){
        scanf("%d%d%d",&op,&x,&y);
        if(x>n || y>n) res++;
        else{
            int px = find(x),py = find(y);
            if(op == 1){
                if(px == py && (d[x] - d[y])%3) res++;
                else if(px != py){
                    p[px] = py;
                    d[px] = d[y]-d[x];
                }
            }
            else{
                if(px == py && (d[x] - d[y] -1)%3) res++;
                else if(px != py){
                    p[px] = py;
                    d[px] = d[y] - d[x] + 1;
                }
            }
        }
    }
    printf("%d\n",res);
    return 0;
}

差分

差分

强烈建议看看力扣今天的每日一题。指路1109题。

也算是实际应用了吧。

对差分的理解就是:

  1. 有一个原数组a[],对于数组a[]上某一段区间内每一个元素进行一个统一的操作,做加法或者减法,如果直接做,那么操作数为这段区间长度。
  2. 如果针对数组a[],构造一个数组b[] 使得a数组为b数组的前缀和数组,那么b数组就为a数组的差分数组。
  3. 那么针对a数组上区间和的操作,映射在数组b上,只需要对b[l] b[r+1]进行两次操作就可以了,因为a数组中a[l]a[r]之间的每个元素都包含元素b[l],a[r+1]a[n]的每个元素都包含b[r+1] 这样就大大减少了操作数。
  4. 最后再对差分数组求一次前缀和,就得到变化后的原数组了。(降维打击了属于是)
#include<iostream>

using namespace std;

const int N = 100010;
int a[N],b[N];
int n,m;

void insert(int l,int r,int c){
    b[l] += c;
    b[r+1] -= c;
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){
        scanf("%d",&a[i]);
    }
    
    for(int i=1;i<=n;++i) insert(i,i,a[i]);
    
    int l,r,c;
    while(m--){
        scanf("%d%d%d",&l,&r,&c);
        insert(l,r,c);
    }
    
    for(int i=1;i<=n;i++) b[i] += b[i-1];
    
    for(int i=1;i<=n;++i) printf("%d ",b[i]);
    
    return 0;
}

堆排序

什么是堆?

  1. 首先堆是维护一个数组集合
  2. 可以插入一个数
  3. 可以求集合中的一个极值
  4. 可以删除这个极值
  5. 删除任意一个元素(STL不能直接干)
  6. 修改任意一个元素(STL不能直接干)

堆 是一棵二叉树(完全二叉树,除了最后一层节点以外所有节点都是满的,最后一层节点是从左向右依次排布的)
小根堆,任意节点一定小于等于其左右儿子。因此根节点是最小值。

时间复杂度: up down 操作都与二叉树树高成正比,为O(logn) 取堆顶? O(1)

本题只要模拟最小堆即可,注意堆化是一个递归的过程,直到这个新插入的节点找到了自己的位置停止。

数组元素一开始的堆化从n/2开始即可,这样初始化的时间复杂度是O(n)的;

#include<iostream>

using namespace std;

const int N = 100010;
int heap[N],bound;

//最小堆的自上而下堆化过程,所以只用完成down()即可
void down(int x){
    int t = x;
    if(2*x <= bound && heap[2*x] < heap[t]) t = 2*x;
    if(2*x+1 <= bound && heap[2*x+1] < heap[t]) t = 2*x+1;
    //终止条件,此节点是它的左右子节点中最小的,此时到位。或抵达边界再无左右节点。否则递归调用堆化过程。
    if(x != t){
        swap(heap[x],heap[t]);
        down(t);
    }
}

int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i) scanf("%d",&heap[i]);
    bound = n;
    
    for(int i = n/2 ; i ; i--) down(i);
    
    while(m--){
        printf("%d ",heap[1]);
        heap[1] = heap[bound];
        bound -- ;
        down(1);
    }
    
}

堆优化
堆优化

针对小根堆的本题,因为多了修改第k个插入的数字,所以在交换时,需要注意维护交换的顺序是一致的,所以用heap_swap来代替简单的swap函数。

此外,与down相对的up函数,up函数只需要比较自身h[x]与其根节点h[x/2]的值的大小关系,所以比较简单,如果能上升则一直进行下去。

注意细节

#include<iostream>
#include<string.h>
#include<algorithm>

using namespace std;
const int N = 100010;
int h[N],ph[N],hp[N],bd;
int n,m,k,x;

void heap_swap(int a,int b){
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a],hp[b]);
    swap(h[a],h[b]);
}

void down(int x){
    int t = x;
    if(2*x <= bd && h[2*x] < h[t]) t = 2*x;
    if(2*x+1 <= bd && h[2*x+1] < h[t]) t = 2*x+1;
    if(t != x){
        heap_swap(t,x);
        down(t);
    }
}

void up(int x){
    while(x/2 && h[x] < h[x/2]){
        heap_swap(x,x/2);
        x >>= 1;
    }
}



int main(){
    char op[5]; 
    scanf("%d",&n);
    while(n--){
        scanf("%s",op);
        //strcmp(s1,s2)相同则返回0 
        if(!strcmp(op,"I")){
            scanf("%d",&x);
            bd++;
            m++;
            ph[m] = bd , hp[bd] = m;
            h[bd] = x;
            up(bd);
        }
        else if(!strcmp(op,"PM")) printf("%d\n",h[1]);
        else if(!strcmp(op,"DM")){
            heap_swap(1,bd);
            bd -- ;
            down(1);
        }
        else if(!strcmp(op,"D")){
            scanf("%d",&k);
            k = ph[k];
            heap_swap(k,bd);
            bd -- ;
            up(k);down(k);
        }
        else{
            scanf("%d%d",&k,&x);
            k = ph[k];
            h[k] = x;
            up(k);down(k);
        }
    }
    return 0;
}

哈希表

模拟哈希表
模拟哈希表

模拟散列表,这里用的是拉链法,哈希运算取模的时候,最好选一个较大的质数,且尽量远离2的整数次幂。

本题,h[k]为该哈希表,e[x]存储的x的值,ne[x]存储x节点的下一跳节点。因为不涉及删除操作,所以直接网里加就行了。
查找时,先由x得出hash(x) = k,然后去查链表e[]上是否存在x

#include<iostream>
#include<cstring>

using namespace std;

const int N = 100003;
int e[N],ne[N],h[N],idx;

void insert(int x){
    int k = (x%N + N)%N;
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx++;
}

bool find(int x){
    int k = (x%N + N) % N;
    for(int j = h[k] ; j != -1 ; j = ne[j]){
        if(e[j] == x) return true;
    }
    return false;
}

int main(){
    int n;
    memset(h,-1,sizeof(h));
    scanf("%d",&n);
    char op[3];
    int x;
    while(n--){
        scanf("%s%d",op,&x);
        if(op[0] == 'I') insert(x);
        else{
            if(find(x)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

开放寻址法:
更好实现,也更好理解。几个要点:

  1. 数组要开到元素个数的2~3倍个,因为哈希冲突难以避免,更多坑位可以更快的找到可用位置。
  2. 要把空数组表示成一个不能被映射到的数据。
  3. 如果在寻找过程中找到了数组的尾端,记得从头开始。
  4. 我记得其他书上看到,开放寻址法的一种改进就是 改变每次搜索的探针长度,在插入操作中可以节省找到新坑位的时间
#include<iostream>
#include<cstring>

using namespace std;

const int N = 200003,NU = 0x3f3f3f3f;
int h[N];

int find(int x){
    int k = (x%N + N)%N;
    while(h[k] != NU && h[k]!=x){
        k++;
        if(k == N) k = 0;
    }
    return k;
}

int main(){
    int n;
    memset(h,0x3f3f3f3f,sizeof(h));
    scanf("%d",&n);
    char op[3];
    int x;
    while(n--){
        scanf("%s%d",op,&x);
        if(op[0] == 'I') {
            h[find(x)] = x;
        }
        else{
            if(h[find(x)] == x) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

字符串哈希
字符串哈希
字符串哈希,很少使用,但是处理字符串匹配好用的。
前提是,我们假设使用这种方法不会带来哈希碰撞!因此这个方法并不是非常稳定的一种算法。

对于一个字符串,从前往后看作是从高位到低位。
把每一个 字符串前缀 都映射成一个P进制的数字,这个数字有可能会非常之大,为了让这个数字映射到一个比较小的区间,我们对他进行取模操作。(使用unsigned long long 来存错,可以避免取模的操作)为了尽量的避免冲突,经验的做法是取P进制的P为 131 或者 13331 .
读入数字,初始化p数组。
p数组存储自高向低每一位的权重, p[i] = p[i-1]*P
对于字符串长度要用到多少p,先预处理出来。
因为每一项都依赖前一项,所以数组下标从1开始 方便又快捷。

#include<iostream>

using namespace std;

typedef unsigned long long ULL;

const int N = 100010 , P = 131;

char str[N];
ULL h[N],p[N];

ULL geth(int l,int r){
    return h[r] - h[l-1]*p[r-l+1];
}

int main(){
    int n,m;
    p[0] = 1;
    scanf("%d%d%s",&n,&m,str+1);
    for(int i=1;i<=n;++i){
        p[i] = p[i-1] * P;
        h[i] = h[i-1] * P + str[i];
    }
    while(m--){
        int l1,r1,l2,r2;
        scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
        if(geth(l1,r1) == geth(l2,r2)) puts("Yes");
        else puts("No");
    }
    
    return 0;
}

搜索与图论

dfs一直学不明白,重中之重了属于是

排列数字
简单的全排列问题,对我而言是意义重大的。
一直搞不明白dfs是什么东西,今天做了这道题,感觉清楚多了。

首先明确要完成的任务,以及怎么衡量这个任务已经完成了(确定递归的参数以及返回值,终止条件)就是怎么样判断一次搜索摸到了底。

比如这道题,要排列n个数,那么当进来的参数x恰好等于的时候就说明此时已经完成了前n个数的排列,那么打印,返回就好。

这道题是怎么保证字典序的?
随着dfs中x的推进,每次对于第x个位置,我都从1遍历到n,去判断哪个数字是可用的,如果可用,那么就写入当前位置path[x]并标记当前被用到的数字i,然后去问下一个位置的数 dfs(x+1),当这个dfs(x+1)执行完毕之后,我知道,当前的i被填入path[x]这个位置之后的所有可能都已经被写完了,那么for循环继续,去寻找当前这个位置可用的下一个可用数字。

每一步都是按照上述步骤来的,所以每次找到的数字最后按照字典序输出。

#include<iostream>

using namespace std;

const int N = 10;

int path[N],n;
bool st[N];

void dfs(int x){
    if(x == n){
        for(int i=0;i<n;++i) printf("%d ",path[i]);
        puts("");
        return;
    }
    for(int i=1;i<=n;++i){
        if(!st[i]){
            path[x] = i;
            st[i] = true;
            dfs(x+1);
            st[i] = false;
        }
    }
}

int main(){
    cin>>n;
    dfs(0);
    return 0;
}

n皇后

n皇后

可以简化为全排列,但是限制条件稍微多一点;好在限制条件之间都是相互独立地,这样的条件越多,越方便剪枝,不是嘛?hhh

对每一列y进行处理,当固定列时,遍历行,需要保证此前这一行未被占用,还要保证两条斜线上未被占用。

两条斜线用dg[] udg[]数组来记录。
遍历过程中,由行i 列y唯一确定的截距,可以用来表征某点的左右斜线。
对于左对角线b1 = i + y
对于右对角线b2 = y - i
为了保证数组下标为正数且映射到0-2n
保证开的数组范围在2N左右。

每次找到这个位置可用的数记得标记,当回溯时记得清除标记。
如果当前位置找不到的话,记得不要立即返回,继续向下比对,去尝试其他的可能。

记得使用puts函数,可以改变二维数组的输出方式。

#include<iostream>

using namespace std;

const int N = 20;

int n;
char g[N][N];
bool col[N],dg[N],udg[N];

void dfs(int y){
    if(y == n){
        foyr(int i=0;i<n;++i) puts(g[i]);
        puts("");
        return;
    }
    
    for(int i=0;i<n;++i){
        if(!col[i] && !dg[y+i] && !udg[n-y+i]){
            col[i] = dg[y+i] = udg[n-y+i] = true;
            g[y][i] = 'Q';
            dfs(yx+1);
            g[x][i] = '.';
            col[i] = dg[y+i] = udg[n-y+i] = false;
            
        }
    }
    
}

int main(){
    cin>>n;
    for(int i=0;i<n;i++){
        for(int j=0;j<n;++j){
            g[i][j] = '.';
        }
    }
    
    dfs(0);
    
    return 0;
    
}

另一种搜索的方法,判断每一个格子是否选择放下一个皇后,若遍历到最后一个格子且放下了8个皇后,则找到一条路.
讲道理,如果每次循环的时候判断一下已经放下了8个皇后,此时直接返回不可以吗?我觉得是可以算作剪枝吧,不过效率不增反降罢了…

#include<iostream>

using namespace std;

const int N = 20;

int n;
char g[N][N];
bool row[N],col[N],dg[N],udg[N];

void dfs(int x,int y,int s){
    if(x == n) x = 0, y++;
    if(y == n){
        if(s == n){
            for(int i=0;i<n;++i) puts(g[i]);
            puts("");
        }
        return;
    }
    
    dfs(x+1,y,s);
    
    if(!row[x] && !col[y] && !dg[x+y] && !udg[n-x+y]){
        g[x][y] = 'Q';
        row[x] = col[y] = dg[x+y] = udg[n-x+y] = true;
        dfs(x+1,y,s+1);
        g[x][y] = '.';
        row[x] = col[y] = dg[x+y] = udg[n-x+y] = false;
    }
}

int main(){
    cin>>n;
    for(int i=0;i<n;i++){
        for(int j=0;j<n;++j){
            g[i][j] = '.';
        }
    }
    
    dfs(0,0,0);
    
    return 0;
    
}

BFS

简单BFS
注意BFS适用于权重都均等的最短路搜索。

bfs相比于dfs的话,dfs可以保证可以搜到一条路,但未必保证最短,bfs每次都从身边开始搜,所以bfs第一次搜到的目的地一定是最短的。

学习了 如何模拟上下左右四个方向上的移动。领教了。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>

using namespace std;

typedef pair<int,int> PII;

const int N = 110;

int n,m;
//存图
int g[N][N];
//存距离
int d[N][N];
int dx[4] = {-1,0,1,0};
int dy[4] = {0,-1,0,1};
queue<PII> q;

int bfs(int x,int y){
    q.push(make_pair(x,y));
    while(!q.empty()){
        PII t = q.front();q.pop();
        for(int i=0;i<4;++i){
            int x = t.first + dx[i],y = t.second + dy[i];
            if(x>=0 && x<n && y>=0 && y<m && g[x][y] == 0 && d[x][y] == 0){
                d[x][y] = d[t.first][t.second]+1;
                q.push(make_pair(x,y));
            }
        }
    }
    return d[n-1][m-1];
}


int main(){
    //memset(d,-1,sizeof d);
    cin>>n>>m;
    for(int i=0;i<n;++i){
        for(int j=0;j<m;++j){
            scanf("%d",&g[i][j]);
        }
    }
    q.push(make_pair(0,0));

    cout<<bfs(0,0);
    
    return 0;
}

八数码/数字华容道

bfs的小小应用。 好像还有一个数字华容道也是一样的。

首先是,为什么bfs可以做?

要求x转移到一个特定的位置,每次x只能转移一步。把转移后每一步的状态都表示成状态A、B、C、。。。 就符合了BFS的目的,从一个状态转移到某一特定状态的过程,统计转移用掉的步数。

怎么把一个数组的整个状态记录下来?并统计转移?

压缩成字符串,用字符串就可以记录下来了。
压缩字符串的规则:

原数组中(规模n*m) 坐标 x y 的元素,经压缩后,在字符串中的位置为 3 * m + y;

字符串中下标为 idx 的字符,在原数组中 下标为 x = idx/m y = idx % m;

BFS还有一个问题就是,已经遍历过的节点,可能会再次访问到,如果在此次搜索的过程中找到了一个以前搜到过的位置,忽略它。

以上。

#include<iostream>
#include<string>
#include<queue>
#include<unordered_map>

using namespace std;
string start;
queue<string> q;
unordered_map<string,int> dis;
int dx[4] = {-1,0,1,0},dy[4] = {0,-1,0,1};

int bfs(string start){
    string end = "12345678x";
    q.push(start);
    dis[start] = 0;
    while(!q.empty()){
        string t = q.front();q.pop();
        int d = dis[t];
        if( t == end) return d;
        
        //状态转移
        int k = t.find('x');
        int x = k/3 , y = k%3;
        for(int i=0;i<4;++i){
            int kx = x+dx[i],ky = y+dy[i];
            if(kx >=0 && kx < 3 && ky >=0 && ky <3){
                swap(t[k],t[3*kx + ky]);
                if(!dis.count(t)){
                    dis[t] = d + 1;
                    q.push(t);
                }
                swap(t[k],t[3*kx + ky]);
            }
            
        }
        
    }
    return -1;
    
}


int main(){

    for(int i=0;i<3;i++)
        for(int j=0;j<3;j++){
            char t;
            cin>>t;
            start+=t;
        }
            
            
    cout<<bfs(start);
    
    
    return 0;
    
}

树的重心

树的重心

树的中心,设计dfs(x)的返回值是以当前x为根,它的各个子树中的连通块中 点数 的最大值。那么此根被减去之后,它上方的树单独成为一个连通块,这个连通块种的点数个数为n-当前节点与所有以当前节点为根的连通块数量之和。

每次递归返回之前,记得更新ans,保证以任何一个点作为根节点处理时得到的那个答案是最小的。在过程中更新此答案。

dfs的心得是,写出dfs就绝对相信它可以完成任务,去处理剩下的逻辑,你的内部逻辑也同时被完善了。想多了会乱!千万不要胡思乱想,做好眼前事。

#include<iostream>
#include<cstring>

using namespace std;

const int N = 100010 , M = 2*N;

int n;
int ans = N;
int h[N],e[M],ne[M],idx;
bool st[N];

void add(int a,int b){
    e[idx] = b , ne[idx] = h[a] , h[a] = idx++;
}

int dfs(int x){
    st[x] = true;
    
    int sum = 1,res = 0;
    for(int i = h[x] ; i != -1 ; i = ne[i]){
        int j = e[i];
        if(!st[j]){
            int s = dfs(j);
            res = max(res,s);
            sum += s;
        }
    }
    res = max(res,n-sum);
    ans = min(ans,res);
    return sum;
}




int main(){
    int a,b;
    memset(h,-1,sizeof h);
    cin>>n;
    for(int i=0;i<n-1;++i){
        cin>>a>>b;
        add(a,b),add(b,a);
    }
    dfs(1);
    cout << ans;
    
    return 0;
    
}

图中点的层次

图中点的层次

BFS宽搜。
能使用宽搜,还是要先判断,是否边权重统一为1?

每个节点只遍历一遍,遍历过的节点会更新它到起点的距离,所以判断依据是d[j]是否为-1;

然后就是还要搞懂邻接表,存图的逻辑。

每当搜索到一个节点,都会更新它的值,所以遇到环路,重复的节点距离将被更新,再次遭遇将跳过;当所有可以遭遇的节点都过了一遍,直接输出d[n] 若为-1说明未被更新过,也就是说宽搜没能找到目的地,此目的地是不可达的。

#include<iostream>
#include<cstring>

using namespace std;
const int N = 100010;
int n,m;
int q[N],d[N],e[N],ne[N],h[N],idx;

void add(int a,int b){
    e[idx] = b , ne[idx]= h[a], h[a] = idx++;
}

int bfs(){
    memset(d,-1,sizeof d);
    int tt = -1 , hh = 0;
    q[++tt] = 1;
    d[1] = 0;
    while(hh <= tt){
        int t = q[hh++];
        for(int i = h[t] ; i != -1 ; i = ne[i]){
            int j = e[i];
            if(d[j] == -1){
                d[j] = d[t]+1;
                q[++tt] = j;
            }
        }
    }
    return d[n];
}

int main(){
    memset(h,-1,sizeof h);
    cin>>n>>m;
    for(int i=0;i<m;++i){
        int a ,b ;
        cin>>a>>b;
        add(a,b);
    }
    
    cout<<bfs()<<endl;
    
    return 0;
    
}

Dijkstra朴素

Dijkstra求最短路径1

朴素Dijkstra算法,突出算法的核心思想。

时间复杂度,两重循环,外层循环1-n个点,内层依据第i个点更新i的邻接点,因此为O(N^2)

注意Dijkstra多用于解决稠密图的情况,对于稠密图,使用邻接矩阵来存储;而对于稀疏图,采用邻接表。

稠密图与稀疏图

稠密图: 边的条数 E 接近于 V^2

稀疏图: 边的条数远远小于 V^2

Dijkstra每次循环都确定一个点的最短距离(它距离起点的最短距离)

因为存在自环,重边的问题,所以建表的时候,记得选取边权最小的情况;此外使用一个bool矩阵来表征某个节点是否已经被确定了最短路径。(这个距离可能会被新加入的节点推翻,所以这不是绝对的最短距离)

初始化邻接表的时候,常用的==memset()==函数:

头文件 #include

void *memset(void *s , int t , size_t n)

  1. 将s中前n个字节 (typedef unsigned int size_t )用 ch 替换并返回 s 。
  2. 其实这里面的ch就是ascii为ch的字符;
  3. 将s所指向的某一块内存中的前n个 字节的内容全部设置为ch指定的ASCII值
  4. 要注意这里的第三个参数是以头指针开始的n个字节单位,所以常用sizeof(数组名)来表示需要填充的字节长度。(好用极了!)
  5. memset()按照字节赋值 一个int4个字节 所以有 4 个3f 为 0x3f3f3f3f

那么为什么采用0x3f3f3f3f代替正无穷?

0x3f3f3f3f + 0x3f3f3f3f 首先不会越界 符合正无穷+正无穷 仍然是正无穷
0x3f3f3f3f + 一个不那么大的整数 符合正无穷 + 正整数仍为正无穷

#include<iostream>
#include<cstring>

using namespace std;

const int N = 510;
int n,m;
int g[N][N];
int dist[N];
bool st[N];

int dijkstra(){
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    for(int i=0;i<n;++i){
        int t = -1;
        //遍历所有未确定最短路径的点。
        for(int j=1;j<=n;++j){
        //当找到j未被确定为最小点,若此时t==-1 则直接将j赋给t,然后确定剩下所有点与当前t的关系,保证这一圈循环下来,dist[t]是当前未确定最短路径的点中路径最短的呢那个点。
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        }
        //将t加入集合st。
        st[t] = true;
        //循环一遍,以 起始点到t,t点再到j的距离 与j原定最小距离相比,更新所有的最小距离。(
        不要加st[j]的判断,会跳过一些原本符合更新条件的情况)
        for(int j = 1;j<=n;++j){
            dist[j] = min(dist[j] , dist[t]+g[t][j]);
        }
    }
    return dist[n] == 0x3f ? -1 : dist[n];
}




int main(){
    memset(g,0x3f,sizeof g);
    scanf("%d%d",&n,&m);
    while(m--){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        g[a][b] = min(g[a][b],c);
    }
    int t = dijkstra();
    
    printf("%d",t);
    return 0;
}

Dijkstra的堆优化

Dijkstra求最短路径2

再朴素的Dijkstra基础上添加了堆优化,省去了对自环,重边的判断。
时间复杂度控制 在mlong(n)

稠密图和稀疏图的选择

稠密图 边的个数在点个数平方 数量级左右。
稀疏图 边的个数远远小于点的个数平方数量级。

对于队列存储的pair类型数据,总是先比较第一位,再比较第二位的。
因此设计队列时,==将路径放在前面,节点放在后面,==保证总是最小路径先出队,然后当一个结点的最小 路径出队以后,更新标记数组st[] 这样重复的边就会被跳过了。

稀疏图记得要多一个数组来存储边权。

Dijkstra方法不能处理负边权的情况。

#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
#include<algorithm>

using namespace std;

typedef pair<int,int> PII;


const int N = 1000010;

int e[N],h[N],ne[N],w[N],idx;
int n,m;
int dist[N];
bool st[N];

void add(int a,int b,int c){
    e[idx] = b , ne[idx] = h[a] , w[idx] = c , h[a] = idx++;
}

int dijkstra(){
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    priority_queue<PII,vector<PII>,greater<PII>> pq;
    //<距离,节点>
    pq.push({0,1});
    while(!pq.empty()){
        PII tnode = pq.top();pq.pop();
        
        int dt = tnode.first , via = tnode.second;
        if(st[via]) continue;
        //在这里更新之后,下次访问将跳过循环,节省时间,优化就体现在这里了
        st[via] = true;
        
        for(int i = h[via] ; i != -1 ; i = ne[i]){
            int j = e[i] ;
            if(dist[j] > dt + w[i]){
                dist[j] = dt + w[i];
                pq.push({dist[j],j});
            }
        }
    }
    
    return dist[n] == 0x3f3f3f3f ? -1 : dist[n];
}


int main(){
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);
    while(m--){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    int t = dijkstra();
    printf("%d",t);
    return 0;
}

Bellman_Ford算法

有边数限制的最短路

bellman_ford 可以解决有负权边的问题,这是Dijkstra所不能及的。

  1. 首先初始化dist为正无穷,然后令dist[1] = 0;
  2. 循环k次(有限制的边数),拷贝上一次的dist数组情况,这是为了防止一次更新了多条边的串联情况。
  3. 然后内循环m条边,对每一个当前节点,用备份数据的起点最短路径+权重 与 但当前最短路径比较 取最小值。最后返回即可。

一个小问题,因为最终得到的边权结果可能为负,所以不能在输出的时候简单判断,然后用-1来代替找不到最短路的情况,可以在最后输出的时候再进行判断。

#include<iostream>
#include<cstring>

using namespace std;

const int N = 510,M = 10010;
int n,m,k;
int dist[N],backup[N];

struct{
    int a,b,w;
}edges[M];

int bellman_ford(){
    memset(dist , 0x3f , sizeof dist);
    dist[1] = 0;
    for(int i=0;i<k;++i){
        memcpy(backup,dist,sizeof dist);
        for(int j=0;j<m;++j){
            int a = edges[j].a , b = edges[j].b , w = edges[j].w;
            dist[b] = min(dist[b] , backup[a] + w);
        }
    }
    return dist[n];
}

int main(){
    scanf("%d%d%d",&n,&m,&k);
    for(int i=0;i<m;++i){
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        edges[i] = {a,b,w};
    }
    
    int res = bellman_ford();
    if(res > 0x3f3f3f3f / 2) puts("impossible");
    else printf("%d\n",res);
    
    return 0;
}

SPFA

SPFA求最短路

spfa 不能处理有负环的图,除此以外,它的方方面面都比bellman_ford要优

类似Dijkstra 优化算法的写法,注意在循环遍历的时候,不再需要备份了,我们只对有资格更新的边进行更新,因此简化了时空复杂度。

这几个算法要注意的问题:

  1. dist数组记得更新
  2. 注意标记数组st[]的值的更新
  3. bellman_ford算法每次更新前需要拷贝操作。
  4. bellman可以处理负环,最适合做的题目是有边数限制的条件。
  5. spfa求最短路方方面面性能是优于bellman_ford的,特点就是使用了容器优化queue 优先队列等都可以实现。目的就是忽略掉不必要的更新。
#include<iostream>
#include<queue>
#include<cstring>

using namespace std;

const int N = 100010 , M = 100010;

int e[N],ne[N],w[N],h[N],idx;
int dist[N],n,m;
bool st[N];

void add(int a,int b,int c){
    e[idx] = b , w[idx] = c , ne[idx] = h[a] , h[a] = idx++;
}

int spfa(){
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    queue<int> q;
    q.push(1);
    st[1] = true;
    while(!q.empty()){
        int t = q.front();q.pop();
        st[t] = false;
        for(int i=h[t]; i!=-1 ; i = ne[i]){
            int j = e[i];
            if(dist[j] > dist[t] + w[i]){
                dist[j] = dist[t] + w[i];
                if(!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return dist[n];
}

int main(){
    memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    while(m--){
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        add(a,b,w);
    }
    int res = spfa();
    if(res > 0x3f3f3f3f / 2) puts("impossible");
    else printf("%d\n",res);
    
    return 0;
}

SPFA判断负环

SPFA中 dist[]数组记录1号点到当前点之间的最短距离
额外再附加一个cnt[]数组 用来记录到达当前点的边数。
假设对所有节点,创建一个虚拟源点,它可以连接到任意一个点,且边权值均为0,那么以此点为起点,更新一遍以后,队列中的元素就拓展为所有点,等价于 初始化时将所有节点都放入队列。dist数组记录当前节点到虚拟节点的路径,cnt数组记录当前节点到虚拟节点最短路的边数,若边数大于等于n 说明一定有n+1个点,那么至少有一个点被重复走了,所以存在负环。

令,邻接表的板子真好用啊!

更新时:

dist[x] = dist[t] + w[i]
cnt[x] = cnt[t] + 1

#include<iostream>
#include<cstring>
#include<queue>

using namespace std;

const int N = 100010;
int e[N],ne[N],h[N],w[N],idx;
int dist[N],cnt[N];
int n,m;
bool st[N];

void add(int a,int b,int c){
    e[idx] = b , ne[idx] = h[a] , w[idx] = c ; h[a] = idx++;
}

bool spfa(){
    queue<int> q;
    for(int i=1;i<=n;i++) {
        st[i] = true;
        q.push(i);
    }
    
    while(q.size()){
        int t = q.front();q.pop();
        st[t] = false;
        
        for(int i=h[t] ; i!=-1 ; i=ne[i]){
            int j = e[i];
            if(dist[j] > dist[t] + w[i]){
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if(cnt[j] >= n) return true;
                if(!st[j]){
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main(){
    memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    while(m--){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    if(spfa()) puts("Yes");
    else puts("No");
    return 0;
}

Floyd

Floyd是求多元汇最短路问题的算法,据说是超级简单。
Floyd求最短路问题

Floyd

  1. 主要是解决多源汇路径的问题
  2. 用邻接矩阵来存图,经一次floyd()处理,将d[i][j]处理为记录i-j之间最短路的矩阵
  3. 核心
for(int k=1;k<=n;k++)
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            d[i][j] = min(d[i][j] , d[i][k] + d[k][j]);

Floyd的原理是基于动态规划的 时间复杂度O(n^3) 比较笨但是实用的算法

#include<iostream>

using namespace std;

const int N = 210,INF = 0x3f3f3f3f;
int d[N][N];
int n,m,q;

void floyd(){
    for(int k=1;k<=n;++k)
        for(int i=1; i<=n;++i)
            for(int j=1;j<=n;++j)
                d[i][j] = min(d[i][j] , d[i][k] + d[k][j]);
}

int main(){
    scanf("%d%d%d" , &n,&m,&q);
    
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(i == j) d[i][j] = 0;
            else d[i][j] = INF;
        }
    }
    
    while(m--){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        d[a][b] = min(d[a][b],c);
    }
    floyd();
    while(q--){
        int a,b;
        scanf("%d%d",&a,&b);
        if(d[a][b] > INF/2) puts("impossible");
        else printf("%d\n",d[a][b]);
    }
    return 0;
}

PRIM最小生成树

PRIM最小生成树,与DIJIKSTRA有点相似。

#include<iostream>
#include<cstring>

using namespace std;

const int N = 510,INF = 0x3f3f3f3f;
int n,m;
int g[N][N],dist[N];
bool st[N];

int prim(){
    //把所有距离初始化成正无穷。
    memset(dist,0x3f,sizeof dist);
    int res = 0;
    for(int i=0;i<n;++i){
        //标识符t,若t==-1说明还未找到任何一个点
        int t = -1;
        for(int j=1;j<=n;++j){
            //t == -1 说明还未找到任何一个点,那么将第一个点j加入,或者找到了距离更小的点j,用j来更新t
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;res
        }
        //如果对于当前i为0即第一次尝试时,加入一个点但并不更新res,只是更新其他点到集合的距离
        if( i && dist[t] == INF) return INF;
        if(i) res += dist[t];
        //用t更新所有不在集合中的点到集合的距离
        for(int j=1;j<=n;++j) dist[j] = min(dist[j] , g[t][j]);
        st[t] = true;
    } 
    return res;
}

int main(){
    scanf("%d%d",&n,&m);
    //需要将所有边权初始化为正无穷
    memset(g,0x3f,sizeof g);
    while(m--){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        //无向图,加边加两条边,存在重边,所以还要取最小边
        g[a][b]  = g[b][a] = min(g[a][b] , c);
    }
    int ans = prim();
    if(ans == INF) puts("impossible");
    else printf("%d\n",ans);

    return 0;
    
}

Kruskal求最小生成树

Kruskal最小生成树

Kruskal 思路还是比较简单的,可以用结构体存所有的边,然后重载运算符 < 调用库函数sort按照边的权值从小到大进行排序。

然后从小到大遍历每条边,用cnt统计已经联通的点的数量,用res记录已经联通的所有点的边权之和;如果发现边的两个端点a,b不属于同一个集合(用并查集来看,两个根节点不相等则不在同一个集合)就把这两个点并入集合。cnt++ ;res += w;最后返回时统计一下,如果集合中点的个数不足n个,说明不是所有的点都联通了,无法得到最小生成树。返回INF。

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 100010 , M = 200010 , INF = 0x3f3f3f3f;

int n,m;
int p[N];

struct Edge{
    int a,b,w;
    bool operator< (const Edge &W)const{
        return w < W.w;
    }
}edges[M];

int find(int x){
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int kruskal(){
    sort(edges,edges+m);
    for(int i=1;i<=n;++i) p[i] = i;
    
    int res = 0,cnt = 0;
    for(int i=0;i<m;++i){
        int a,b,w;
        a = edges[i].a , b = edges[i].b , w = edges[i].w;
        a = find(a) , b = find(b);
        if(a!=b){
            p[a] = b;
            res += w;
            cnt ++;
        }
    }
    return cnt < n-1 ? INF : res;
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;++i){
        int a,b,w;
        scanf("%d%d%d",&a,&b,&w);
        edges[i] = {a,b,w};
    }
    
    int t = kruskal();
    if(t == INF) puts("impossible");
    else printf("%d\n",t);
    
    return 0;
}

染色法判断二分图

染色法判断二分图

Q: 什么是二分图?
A: 二分图也叫二部图,设G = (V,E) 是一个无向图,若顶点V可分割为两个互不相交的子集,且图中的每条边(i,j)所关联的两个顶点i j 分别属于这两个不同的顶点集(i in A , j in B),则符合图G为二分图

换句话说,一个无向图中的所有点可以被分到分隔到两个不同的集合中,且图中的每一条边依附的两个端点都分别属于这两个互不相交的子集,两个子集各自内部的顶点却并不相邻。

染色法证明二分图的核心思想是 == 一张图为二分图,那么该图内一定不包含奇数环 == 无需额外处理重边与自环,直接记录。

dfs深度遍历实现,每次找到一个顶点,将其染色,然后遍历他所有邻接点,并将临界点染成另一种颜色,直到链条上所有邻接点都被染色。过程中,一旦遇到已经被染色的节点,判断该点与当前点的颜色是否相异,若相同说明存在了奇数环,那么判断染色失败。一旦染色失败,说明改图不是二分图。

#include<iostream>
#include<cstring>

using namespace std;

const int N = 100010 , M = 200010;
int e[M],ne[M],h[N],idx,n,m;
int color[N];

void add(int a,int b){
    e[idx] = b , ne[idx] = h[a] , h[a] = idx++;
}

bool dfs(int x , int c){
    color[x] = c;
    for(int i = h[x] ; i != -1 ; i = ne[i]){
        int j = e[i];
        if(!color[j]){
            if(!dfs(j,3-c)) return false;
        }
        else if(c == color[j]) return false;
    }
    return true;
}


int main(){
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);
    
    while(m--){
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b),add(b,a);
    }
    
    bool flag = true;
    for(int i=1;i<=n;i++){
        if(!color[i]){
            if(!dfs(i,1)){
                flag = false;
                break;
            }
        }
    }
    if(flag) puts("Yes");
    else puts("No");
    
    return 0;
}

匈牙利算法求二分图最大匹配

匈牙利算法求二分图最大匹配

Y总的恋爱哲学之匈牙利算法

对于一个给定的二分图,求它的最大匹配。就是找到两个集合的所有顶点之间不冲突的配对对数。

匈牙利算法的逻辑是,从某一个集合A中某一个点出发,寻找它在另一个集合B中的可配对端点。若找到,则配对,匹配对数 加加;若找到的端点(B)已经被匹配,则去寻找这个匹配的端点(A)若此端点有其他可匹配端点,则让他走开,你占有。匹配对数加加;

生动形象,真实有趣。

#include<iostream>
#include<cstring>

using namespace std;

const int N = 510 , M = 100010;
int n1,n2,m;
int e[M],h[N],ne[M],idx;
int match[N];
bool st[N];

void add(int n1,int n2){
    e[idx] = n2 , ne[idx] = h[n1] , h[n1] = idx++;
}

//find函数,找到是否存在当前节点可配对的端点
bool find(int x){
    for(int i=h[x] ; i!= -1 ; i = ne[i]){
        int j = e[i];
        if(!st[j]){
            st[j] = true;
            if(match[j] == 0 || find(match[j])){
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}


int main(){
    scanf("%d%d%d",&n1,&n2,&m);
    memset(h,-1,sizeof h);
    while(m--){
        int n1,n2;
        scanf("%d%d",&n1,&n2);
        add(n1,n2);
    }
    int res = 0;
    for(int i=0;i<=n1;i++){
        memset(st,false,sizeof st);
        if(find(i)) res++;
    }
    printf("%d",res);
    
    return 0;
}

暂时告一段落了,消化消化

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值