基本数据结构篇(三万字总结)

本文深入探讨了数据结构中的栈、队列及其应用,如火车进栈问题、单调队列、哈希函数在字符串处理中的应用。通过实例解析了如何利用栈解决编辑器操作、火车进站问题,利用队列解决蚯蚓排序,以及哈希在字符串匹配中的应用。此外,还介绍了KMP算法、Trie树、卡特兰数等概念,展示了它们在字符串处理中的高效解决方案。
摘要由CSDN通过智能技术生成

编辑器(对顶栈)

原题链接

解题思路:
在这里插入图片描述
在这里插入图片描述
解题思路链接:https://www.acwing.com/solution/content/27188/

代码:

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int t,x,sum[N],f[N],now;
stack<int> a,b,c;
int main()
{
    while(scanf("%d\n",&t)!=EOF)//之前在HDU提交,所以是多组数据
    {
        a=c;//STL特性,这里就是清空操作
        b=c;
        f[0]=-1e7;//初始化
        sum[0]=0;
        for(int i=1;i<=t;i++)
        {
            char ch=getchar();//读入
            if (ch=='I')//插入操作
            {
                scanf(" %d",&x);
                a.push(x);//将a插入栈中
                sum[a.size()]=sum[a.size()-1]+a.top();//前1~a.size()-1的前缀和,加上这个一个新来的,构成1~a.size()
                f[a.size()]=max(f[a.size()-1],sum[a.size()]);//看是之前的最大值大,还是新来的最大值大
            }
            if (ch=='D')
                if (!a.empty())//只要栈不为空,就删除
                    a.pop();
            if (ch=='L')//左倾思想(博古+文化大革命)(手动滑稽)
                if(!a.empty())//只要不为空
                    b.push(a.top()),a.pop();//a+b等于整个插入序列,b负责管理当前光标右边的序列.
            if (ch=='R')//右倾思想(陈独秀)(手动滑稽)
            {
                if (!b.empty())//b不为空
                {
                    a.push(b.top());//a负责管理1~当前光标.所以现在a往右了,那么必然是要加入b栈的开头,因为b栈管理当前光标的右边.
                    b.pop();
                    sum[a.size()]=sum[a.size()-1]+a.top();//同样的还是重新定义.
                    f[a.size()]=max(f[a.size()-1],sum[a.size()]);//见插入操作.
                }
            }
            if (ch=='Q')
            {
                scanf(" %d",&x);
                printf("%d\n",f[x]);//输出当前最大值区间.
            }
            getchar();//换行符读入
        }
    }
    return 0;
}


代码链接:https://www.acwing.com/solution/content/1275/

火车进栈

题目链接

解题思路:

如下图,我们在dfs()的时候要维护三个状态,状态一就是出栈的序列,状态二是还在栈里的元素,状态三是还没有进栈的元素。我们在每一个状态下都有两个操作可以进行,第一个操作是将元素进栈,第二个操作是元素出栈。边界条件是state1 的元素个数为n。题目中要我们按字典序的输出方案,因此我们考虑以下操作一与操作二的顺序。因为state3中的元素肯定是比state2中的元素还要大的,因此我们先执行操作二再执行操作一即可。
在这里插入图片描述

代码:

#include<stdio.h>
#include<stack>
#include<vector>

using namespace std;

int n,cnt = 20;
vector<int> state1;
stack<int>state2;
int state3 = 1;

void dfs(){
    if(!cnt)return ;
    if(state1.size() == n){
        cnt --;
        for(auto x : state1) printf("%d",x);
        putchar('\n');
        return ;
    }
    // state2 要满足 栈不为空
    if(!state2.empty()){
        state1.push_back(state2.top());
        state2.pop();
        dfs();
        state2.push(state1.back());
        state1.pop_back();
    }
    // state3 要满足的是元素个数不能大于n
    if(state3 <= n){
    state2.push(state3);
    state3 ++ ;
    dfs();
    state3 -- ;
    state2.pop();
    }
}


int main(){
    scanf("%d",&n);
    dfs();
    
    return 0;
}

火车进栈问题(卡特兰数)

原题链接

解题思路:

本题是一个卡特兰数,相关解释可以百度。卡特兰数的本质是:任何前缀某一类的元素大于等于另一类,如果题目符合这个性质,那么大概率就是卡特兰数了。
首先我们要求高精, ( n 2 n ) n + 1 \frac{\binom{n}{2n}}{n+1} n+1(2nn) 也就是 2 n ! n ! ∗ n ! ∗ ( n + 1 ) \frac{2n!}{n! * n! * (n+1)} n!n!(n+1)2n! 最快的办法是求出这玩意所有的质因数及其次数,然后求所有数乘积即可。同时在高精度乘法的时候压位来提高速度。

解题步骤:

  • 利用素数筛法,求出 2~2n 内的所有素数。
  • 求出 2n! 与 n! 中质因子的个数。
  • 减去 n + 1 中质因子个数。
  • 大数乘法。

知识点:

大数相乘

分解质因数

阶乘分解质因数

压位

代码:

#include<stdio.h>
#include<vector>

using namespace std;

const int N = 1200010;
int primes[N],cnt;
bool st[N];
int power[N];
//素数筛法
void get_primes(int n ){
    cnt = 0;
    for(int i = 2 ;i <= n ;++i)
        if(!st[i]){
            primes[cnt++] = i;
            for(int j = 2 * i ; j <= n ; j += i)
                st[j] = true;
        }
}
// 得到 n! 中 p 因子的个数
int get(int n , int p ){
    int s = 0;
    while(n){
        s += n / p;
        n /= p ;
    }
    return s;
}

void mutil(vector<int>&a,int b){
    int t = 0; // 进位
    int n = a.size();
    for(int i = 0 ; i < n ; ++i){
        a[i] = a[i] * b + t;
        t = a[i] / 10000;
        a[i] %= 10000;
    }
    while(t){
        a.push_back( t % 10000);
        t /= 10000;
    }
}


void out(vector<int> a){
    printf("%d",a.back());
    for(int i = a.size() - 2 ; i >= 0 ; -- i)printf("%04d",a[i]);
    putchar('\n');
}

int main(){
    int n;
    scanf("%d",&n);
   get_primes(2 * n);
     for(int i = 0 ; i < cnt ; ++i){
       int p = primes[i];
       // 对于计算·每一个质因子 在 n !中的个数
       power[p] = get(2*n,p) - get(n,p) * 2;
    }
    // 对分母的 n + 1 进行质因数分解
    int k = n + 1 ;
    for(int i = 0 ; i < cnt && primes[i] <= k ; ++i  ){
        int p = primes[i], s = 0;
        while(k % p == 0){
            k /= p;
            s++;
        }
        power[p] -= s;
    }
    vector<int>ans;
    ans.push_back(1);
    // 对 还有的质因数进行乘法运算
    for(int i = 0 ; i < cnt ; ++i){
        int p = primes[i];
        for(int j = 0; j < power[p] ; ++j) // 质因数个数
            mutil(ans,p);
    }
    out(ans);
    
    return 0;
}

配套的高精度除法

void div(vector<int>&a,int b){
    int t = 0; // 上一位的余数·
    for(int i = a.size()-1 ; i >= 0 ;++i){
        a[i] += t * 10;    
        t = a[i] % b;
        a[i] /= b;
    }
    // 把高位的0除去
    while(a.size() > 1 && a.back() == 0)a.pop_back();
}

队列

小组队列

题目链接

解题思路:

因为队伍中只要前边有自己队员就不用在队尾排队,所以如果我们用一个对列来实现的话不好弄。因为插入队员后也是一个队列。因为我们会每一个小组建立一个队列。而排队的队列我们存的是组号。当我们要出队时,我么就查询对首值得到其对应的组号,依据这个组号去改组号对应的队列里边进行出队。

  • 进队操作:当一个编号为x的队员进队时,我们得到其对应的组号。然后压入该组号对应的队列里边,如果压入之前队列为空,那么将该组号压入 q0 的队尾。
  • 出队操作:我们读取q0 中对首元素对应的组号,将这个组号的队列的对首元素出队。如果在出队后队列为空,那么就将q0的对首元素出队,表示这个组的成员已经全部出队了。

代码:

#include<stdio.h>
#include<string>
#include<queue>
#include<map>
#include<string>
#include<iostream>
using namespace std;

int t,cnt ;

int main(){
    while(~scanf("%d",&t)){
        if(!t)break;
        cnt ++;
        map<int,int>vis;
        queue<int> q[1010];
        for(int i = 1 , x ,n;i <= t ; ++i){
            scanf("%d",&n);
            while(n -- ){
                scanf("%d",&x);
                vis[x] = i; // 对于同一组的打上标签
            }
        }
        printf("Scenario #%d\n",cnt);
        string s;
        while(cin>>s){
            if( s == "STOP")break;
            if(s == "ENQUEUE"){
                int x,pos;
                scanf("%d",&x);
                pos = vis[x]; // 找到编号对应得组
                if(q[pos].empty())
                    q[0].push(pos);
                q[pos].push(x);
            }
            if(s == "DEQUEUE"){
                int pos = q[0].front();
                printf("%d\n",q[pos].front());
                q[pos].pop();
                if(q[pos].empty())
                    q[0].pop();
            }
        }
        puts("");
    }
return 0;    
}

蚯蚓

题目链接

解题思路:

我们发现如果蚯蚓i被切断后,然后蚯蚓j也被切断,那么我们发现,i蚯蚓的切后两段的长度,都会大于j蚯蚓的切后的两段的长度,因此这里有单调递减的性质.
找到了性质,那么我们完全可以通过三个队列模拟优先队列,一个队列维护切后的第一段,一个队列维护切后的第二段,另外一个队列,里面存储蚯蚓长度,记住长度是从高到低,排好序的长度,那么每一次将被切断的蚯蚓,肯定是这三个队列的队头,因为我们这道题目具有单调递减的性质,所以其实这道题目三个队列,都隐藏着单调队列的性质.

解题步骤

第一步将原始的长度从大到小排个序。之后进行m次操作。每一次取出三个队列对首的最大值x,x在加上偏移量delta后,分成两段后,偏移量加上q后,分别减去偏移量后放进队列中。

技巧:

  • 在本题中有一个对集合进行整体加上一个数的操作。0(n)的思路是把每一个元素都加上。不过有一个O(1)操作是,设置一个偏移量delta,存在集合中的数据是一个相对值,在取出后加上偏移量就得到真实值。然后在放入集合的时候减去偏移量即可。

代码:

#include<stdio.h>
#include<algorithm>
#include<limits.h>

using namespace std;
const int N = 7000010;
typedef long long ll;
ll q1[N] , q2[N] , q3[N];
int h1,h2,h3,t1,t2 = -1,t3 = -1;
int n,m,q,u,v,t;
ll delta;

ll get_max(){
    ll maxx = INT_MIN;
    if(h1 <= t1) maxx = max(maxx,q1[h1]);
    if(h2 <= t2) maxx = max(maxx,q2[h2]);
    if(h3 <= t3) maxx = max(maxx,q3[h3]);
    if(h1 <= t1 && q1[h1] == maxx) h1++;
    else if(h2 <= t2 && q2[h2] == maxx) h2++;
    else h3++;
    return maxx;
}

int main(){
    scanf("%d%d%d%d%d%d",&n,&m,&q,&u,&v,&t);
    for(int i = 0 ;i < n ; ++i)
        scanf("%d",&q1[i]);
    sort(q1,q1+n);
    reverse(q1,q1+n);
    t1 = n - 1;
    for(int i = 1 ; i <= m ; ++i){
        ll x = get_max();
        x += delta;
        if(i % t == 0)printf("%d ",x);
        int left = x * 1ll * u /v;
        int right = x - left;
        delta += q;
        q2[++t2] = left - delta;
        q3[++t3] = right - delta;
    }
    puts("");
    for(int i = 1 ; i <= n + m ; ++i){
        ll x = get_max();
        if(i % t == 0)printf("%d ",x + delta);
    }
    puts("");
    return 0;
}

双端队列

题目链接

题解:
以下的大佬总结得比我好,所以就贴上了
题解出处
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

收获:

  • 找出元素相同的区间,while(j < n && a[i].first == a[j].first) j++;
  • 贪心来维护单调性,同时设置一个变量来保存当前的单调性。

代码:

#include<stdio.h>
#include<algorithm>
#include<limits.h>

using namespace std;

typedef pair<int,int>PII;

const int N = 200010;

PII a[N];
int n;

int main(){
    scanf("%d",&n);
    for(int i = 0 ; i < n ;++i){
        scanf("%d",&a[i].first);
        a[i].second = i;
    }
    sort(a,a+n);
    
    int res = 1 , last = INT_MAX ;
    int dir = -1 ;
    for(int i = 0 ; i  < n ; ){
        int j = i , minp , maxp;
        while(j < n && a[i].first == a[j].first) j++;
        minp = a[i].second , maxp = a[j - 1].second;
        if(dir == -1 ){
            if(last > maxp) last = minp; // 在接的时候是反过来接的,这样才可以让接的这一段单调减
            else dir = 1 , last = maxp;
        }else{
            if(last < minp) last = maxp;//同理
            else res++,last = minp , dir = -1;// 不能接上的话,新建立的是递减的所以要反着接
        }
        i = j;
    }
    
    printf("%d\n",res);
    return 0;
}

最大子序和(单调队列)

题目链接

解题思路:
在这里插入图片描述

简单来说,我们先利用前缀和数组,将题目转变为数组中两个元素位置相距不大于m的情况下使得后者减去前者得到的值最大。那么我们就转变为每遍历一个右端点的时候,找到前m个元素的最小值。这样就是找到了以该右端点的最大值。如果平常思路是遍历前m个元素找到最小值。不过我们通过单调递增队列维护一个不超过m个元素的队列。该队列的对首的小标就是以i为右端点前m个元素中最小值所在的下标。

代码:

#include<stdio.h>
#include<limits.h>
#include<algorithm>

using namespace std;

typedef long long LL;

const int N = 300010;

LL sum[N];
int q[N];
int n , m ;

int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1 ; i <= n ;++i)
    {
        scanf("%lld",&sum[i]);
        sum[i] += sum[i-1] ;
    }
    int hh = 0 ,tt = 0;                                                                                                
    LL res = INT_MIN;
    for(int i = 1 ; i <= n ; ++i){
        if(hh <= tt && q[hh] < i - m) hh++; // i - m 得到第 m + 1 个元素的位置
        res = max(res,sum[i] - sum[q[hh]]);
        // 因为要维护一个单调递增队列。
        while(hh <= tt && sum[q[tt]] >= sum[i]) tt--; // 因为这里求值就是利用前缀和来的,可以把前缀和看成一个元素就好
        q[++tt] = i;
    }
    printf("%lld\n",res);
    
    return 0;
}

哈希

雪花雪花雪花(序列的最小表示)

题目链接

本题大致题意:

给了n和长度为6的序列,判断两个序列是否相识。可以进行的操作是旋转:即将序列的第一个放到最后一个。另一个操作是翻转。

正确解法应该是用哈希的,不过可以用别的方法解决。本文介绍另一种解法。哈希解法
解题思路:
先介绍以下 序列的最小表示

长度为n的序列的最小表示:一个元素为n的序列旋转n次为得到原序列。在旋转过程中产生的序列中,字典序最小的序列就是该序列的最小表示。

因此,如果本题的操作只有旋转,我们依次求出每一个序列的最小表示。比较是否有相同的,有则说明有相似的序列。不过本题还有一个翻转操作,那么我们同时还求出序列翻转之后的最小序列。在这两个序列中找最小的,比较其是否相同。

技巧:

  1. 求序列长为n的最小序列:
// 求长度为n 的序列的最小表示
void get_min(int a[]){
    static int b[2*n];
    // 复制
    for(int i = 0 ; i < n; ++i)b[i] = a[i % n];
    int i = 0 , j = 1 , k;
    while(i < n && j < n){
        for(k = 0 ; k < n && b[i+k] == b[j+k] ; ++k);
        if(k == n)break;
        if(b[i+k] > b[j+k]){
            i += k + 1;
            if(i == j ) ++i;
        }else{
            j += k + 1;
            if(i == j) ++j;
        }
    }
    k = min(i,j);
    for(int i = 0 ; i < n ; ++i)a[i] = b[i+k];
}
  1. 当对序列进行排序的时候,一般不直接对序列进行排序。而是对其下标进行排序。
  2. 在比较是否有元素相同时,可以先排序,看相邻元素是否相同即可。

代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<stdio.h>
using namespace std;

const int N = 1E5 + 10;

int snows[N][6],idx[N];
int n ;

bool cmp_array(int a[] , int b[]){
    for(int i = 0 ;i < 6 ; ++i )
        if(a[i] > b[i])
            return false;
        else if( a[i] < b[i])
            return true;
    return false;
}

bool cmp(int a , int b){
    return cmp_array(snows[a],snows[b]);
}

// 求长度为6 的序列的最小表示
void get_min(int a[]){
    static int b[12];
    // 复制
    for(int i = 0 ; i < 12 ; ++i)b[i] = a[i % 6];
    int i = 0 , j = 1 , k;
    while(i < 6 && j < 6){
        for(k = 0 ; k < 6 && b[i+k] == b[j+k] ; ++k);
        if(k == 6)break;
        if(b[i+k] > b[j+k]){
            i += k + 1;
            if(i == j ) ++i;
        }else{
            j += k + 1;
            if(i == j) ++j;
        }
    }
    k = min(i,j);
    for(int i = 0 ; i < 6 ; ++i)a[i] = b[i+k];
}

int main(){
    scanf("%d",&n);
    int snow[6],isnow[6];
    for(int i = 0 ; i < n ; ++ i ){
        for(int j = 0 , k = 5 ; j < 6 ; ++j , --k){
            scanf("%d",&snow[j]);
            isnow[k]  = snow[j];
        }
        get_min(snow);
        get_min(isnow);
        if(cmp_array(snow,isnow)) memcpy(snows[i],snow , sizeof snow);
        else memcpy(snows[i] , isnow , sizeof isnow);
        
        idx[i] = i;
    }
    sort(idx,idx + n , cmp);
    bool flag = false;
    for(int i = 1 ; i < n ; ++i)
        if(!cmp(idx[i],idx[i-1]) && ! cmp(idx[i-1],idx[i]))
        {
            flag = true;
            break;
        }
    if(flag)puts("Twin snowflakes found.");
    else puts("No two snowflakes are alike.");
    
    return 0;
}

兔子与兔子

题目链接

解题思路:

这题没啥好说的了,就是一个字符串哈希的模板题。

代码:

#include<stdio.h>
#include<cstring>

using namespace std;

typedef unsigned long long ULL;
const int N = 1E6  + 10;

char s[N];
ULL p[N],h[N]; // 记得利用unsigned long long


int m;

int main(){
    scanf("%s",s+1);
    scanf("%d",&m);
    int len = strlen(s+1);
    p[0] = 1; // 131^0
    for(int i = 1 ; i <= len ; ++i){
        h[i] = h[i-1] * 131 + (s[i] - 'a' + 1);//hash 1~i
        p[i] = p[i-1] * 131;//131^i
    }
    int l1 , r1 ,l2,r2;
    for(int i = 1 ; i <= m ; ++i){
        scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
        int t1 = r1 - l1 + 1 , t2 = r2 - l2 + 1;
        if((h[r1] - h[l1 - 1] * p[t1]) == (h[r2] - h[l2-1] * p[t2]))
        puts("Yes");
        else
        puts("No");
    }
    
    return 0;
}

回文子串的最大长度

题目链接

题解:
在这里插入图片描述
在这里插入图片描述
来自:https://www.acwing.com/solution/content/30482/

收获:

  1. 在一个序列中每两个值之间插入一个值的做法:
for(int i = 2 * n ; i > 0 ; i -= 2 ){
            str[i] = str[i/2];
            str[i-1] = 'z' + 1;
        }
  1. 逆序哈希求法:hr[i] = hr[i-1] * base + str[j] - 'a' + 1;,其中j是从后到前遍历。
  2. 对于长度为n,,中间节点为i的序列的半径大小为:r = min(i-1,n-i);
  3. 在插入值之后,对于一个长度为2j + 1 的序列,如何求出原来的真实长度,分为两种情况,如果边界是原字符,则原字符长度比插入值大1,否则插入值比原字符大1.
// 是L 不是 1
if(str[i - l] <= 'z') res = max(res,l+1);
else res = max(res,l);

注意:本题二分,是找 小于等于一个半径长度r的最大的一个(即r或r的前驱)所以使用的二分方法是:

 while(l < r ){
     int mid = (l + r + 1 ) / 2;
   if(get(hl,i - mid , i - 1) != get(hr,n - (i + mid)+1,n - (i + 1) +1)) r = mid - 1;
        else l = mid ;
            }

代码:

#include<stdio.h>
#include<algorithm>
#include<cstring>

using namespace std;

typedef unsigned long long ULL;
const int N =  2 * 1E7 + 10 ,base = 131;
char str[N];
ULL hl[N],hr[N],p[N];

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

int main(){
    int T = 1 ;
    while(scanf("%s",str+1),strcmp(str+1,"END")){
        int n = strlen(str+1);
        for(int i = 2 * n ; i > 0 ; i -= 2 ){
            str[i] = str[i/2];
            str[i-1] = 'z' + 1;
        }
        n *= 2;
        p[0] = 1;
        for(int i = 1 ,j = n; i <= n ; ++ i,j--){
            hl[i] = hl[i-1] * base + str[i] - 'a' + 1;
            hr[i] = hr[i-1] * base + str[j] - 'a' + 1;
            p[i] = p[i-1] * base;
        }
        int res = 0;
        for(int i = 1 ; i <= n ; ++i){
            int l = 0 , r = min(i-1,n-i);
            while(l < r ){
                int mid = (l + r + 1 ) / 2;
                if(get(hl,i - mid , i - 1) != get(hr,n - (i + mid)+1,n - (i + 1) +1)) r = mid - 1;
                else l = mid ;
            }
            if(str[i - l] <= 'z') res = max(res,l+1);
            else res = max(res,l);
        }
        printf("Case %d: %d\n",T++,res);
    }
    
    
    return 0;
}

KMP

周期(next 数组的性质)

题目链接

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
题解出处:https://www.acwing.com/solution/content/28244/

代码:

#include<stdio.h>

const int N = 1E6 + 10;
char str[N];
int Next[N];
int n ;
// 求next数组模板。
void get_next(){
    Next[1]  = 0;
    for(int i = 2 , j = 0; i <= n ; ++i){
        while(j && str[i] != str[j+1])j = Next[j];
        if(str[i] == str[j+1]) j++;
        Next[i] = j ;
    }
}

// 对于KMP 一般下标从1开始
int main(){
    int T = 1 ;
    while(~scanf("%d",&n) && n){
        scanf("%s",str+1);
        get_next();
    printf("Test case #%d\n",T++);
    for(int i = 2 ;  i <= n ; ++i){
        int t = i - Next[i];
        if(t != i && i % t == 0)printf("%d %d\n",i,i/t);
    }
    puts("");
    }
    return 0;
}

Trie

前缀统计

题目链接

题目大意:先给了n个字符串,之后给m个字符串,在这m个字符串中询问其前缀出现在n个字符串的个数。暴力的做法就是对于每一个前缀依次与n个字符串进行比较O(n2)的。而这里涉及到了字符串得匹配问题,而trie树恰可以解决这个问题。我们利用前n个字符串建立一个Trie树,之后对于m个字符串,依次将这个字符串放到Trie树里边,沿着其前缀走一篇,如果经过的一个节点又结尾标记,就加上1.

题解:
在这里插入图片描述

  • 记录结尾个数的方法就是,额外建立一个数组,将每一次插入后最后的指针p,对应的位置加1.

对于查找字符串是否存在的话,

void insert(){
    int len = strlen(str);
    int p = 0;
    for(int i = 0 ; i < len ; ++i){
        int &s = trie[p][str[i] - 'a']; // 引用类型
        if(!s)s = ++tot;
        p = s;
    }
    end[p] = true; 
}

int query(){
    int p = 0 ;
    int len = strlen(str);
    for(int i = 0 ; i < len ; ++i){
        int &s = trie[p][str[i] - 'a'];
        if(!s)return false;
        p = s;
    }
    return end[p];
}

代码:

#include<stdio.h>
#include<cstring>
const int N = 1E6 + 10 , M = 5E5 + 10;

int n , m;
char str[N];
int trie[M][26],end[M],tot;

void insert(){
    int len = strlen(str);
    int p = 0;
    for(int i = 0 ; i < len ; ++i){
        int &s = trie[p][str[i] - 'a']; // 引用类型
        if(!s)s = ++tot;
        p = s;
    }
    end[p] ++; // 如果只是做一个结尾标记的话可以用bool 数组,这里设置为true
}

int query(){
    int p = 0 , res = 0;
    int len = strlen(str);
    for(int i = 0 ; i < len ; ++i){
        int &s = trie[p][str[i] - 'a'];
        if(!s)break;
        p = s;
        res += end[p];
    }
    return res;
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i = 0 ; i < n ; ++i){
        scanf("%s",str);
        insert();
    }
    for(int i = 0 ; i < m ; ++ i){
        scanf("%s",str);
        printf("%d\n",query());
    }
    
    
    return 0;
    
}

最大异或对

题目链接

解题思路;
(暴力枚举) O(n2)

for (int i = 0; i < n; i++)
{
    for (int j = 0; j < n; j++)
    {
        // 但其实 a[i] ^ a[j] == a[j] ^ a[i]
        // 所以内层循环 j < i // 因为 a[i] ^ a[i] == 0 所以事先把返回值初始化成0 不用判断相等的情况
    }
}

优化算法:
(trie树)
trie树中要明确两个问题:

son[N][x]是个啥?idx是个啥?

首先son[N][x]这是个二维数组。

第一维N是题目给的数据范围,像在trie树中的模板题当中N为字符串的总长度**(这里的总长度为所有的字符串的长度加起来)**,在本题中N需要自己计算,最大为N*31(其实根本达不到这么大,举个简单的例子假设用0和1编码,按照前面的计算最大的方法应该是4乘2=8但其实只有6个结点)。

第二维x代表着儿子结点的可能性有多少,模板题中是字符串,而题目本身又限定了均为小写字母所以只有26种可能性,在本题中下一位只有0或者1两种情况所以为2。

而这个二维数组本身存的是当前结点的下标,就是N喽,所以总结的话son[N][x]存的就是第N的结点的x儿子的下标是多少,然后idx就是第一个可以用的下标。
链接:https://www.acwing.com/solution/content/6156/

代码:

#include<stdio.h>
#include<algorithm>

using namespace std;

const int N  = 1E5 + 10 , M = 5e6 + 10;

int trie[M][2],tot;
int a[N];
int n ;

void insert(int x){
    int p = 0;
    for(int i = 30 ; ~i ; --i){
        int &s = trie[p][x >> i & 1];
        if(!s)s = ++tot; // 创建新节点
        p = s;
    }
}

int query(int x){
    int res = 0 ,  p = 0;
    for(int i = 30 ; ~i ; --i){
        int s = x>>i & 1;
        if(trie[p][!s]){
            res += 1<<i; // 将res二进制第i位置为1
            p = trie[p][!s];
        }else{
            res += 0<<i;// 没有用,可以不用
            p = trie[p][s];
        }
    }
    return res;
}


int main(){
    scanf("%d",&n);
    for(int i = 0 ; i < n ;++i)
    {
        scanf("%d",&a[i]);
        insert(a[i]);
    }
    int res = 0;
    for(int i = 0 ; i < n ; ++i){
        res = max(res, query(a[i]));
    }
    printf("%d\n",res);
    
    return 0;
}

最长异或值路径

题目链接

解题思路:
在这里插入图片描述
在这里插入图片描述
难点:本题在上题的基础上,应该是dfs是一个难点。因为可能是一个多叉树,所以我们通过建图来dfs。同时因为是一个无向图,一个节点的出边有一条是连向其父节点的,因此我们为了避免这种情况在dfs的时候把父节点的信息带过来。

代码:

#include<stdio.h>
#include<algorithm>
#include<cstring>
using namespace std;

const int N  = 1E5 + 10 ,M = 5e6 + 10;

int head[N] , Next[N*2] , e[N*2],c[N*2],cnt;
int trie[M][2],tot;
int a[N];
int n ;

//e 放的是边的终点,c是边权,
void add(int u , int v ,int w){
    e[cnt] = v;c[cnt] = w ;Next[cnt] = head[u] ; head[u] = cnt++;
}

void dfs(int u,int father , int sum){
    a[u] = sum;
    // 遍历父节点的所有出边
    for(int i = head[u] ; ~i ; i = Next[i]){
        int j = e[i]; // 下一个节点
        // 不能返回,即不看是其父节点
        if(j != father) dfs(j,u,sum ^ c[i]);
    }
    
}

void insert(int x){
    int p = 0;
    for(int i = 30 ; ~i ; --i){
        int &s = trie[p][x >> i & 1];
        if(!s)s = ++tot;
        p = s;
    }
}

int query(int x){
    int res = 0 ,  p = 0;
    for(int i = 30 ; ~i ; --i){
        int s = x>>i & 1;
        if(trie[p][!s]){
            res += 1<<i;
            p = trie[p][!s];
        }else{
            res += 0<<i;// 没有用,可以不用
            p = trie[p][s];
        }
    }
    return res;
}


int main(){
    scanf("%d",&n);
    memset(head,-1 ,sizeof head);
    for(int i = 0 , u , v , w ; i < n-1 ;++i)
    {
       scanf("%d%d%d",&u,&v,&w);
       add(u,v,w),add(v,u,w);
    }
    dfs(0,-1,0);
    for(int i = 0 ;i < n ; ++i)insert(a[i]);
    int res = 0;
    for(int i = 0 ; i < n ; ++i){
        res = max(res, query(a[i]));
    }
    printf("%d\n",res);

    return 0;
}

二叉堆

题目链接

解题思路:
在这里插入图片描述
在这里插入图片描述
解题步骤:

  1. 将数据读入,按照时间从小到大进行排序。
  2. 建立小根堆(存利润),遍历一边所有商品。分为两种情况。如果商品过期天数大于推中元素个数直接插入,如果等于堆中商品个数,如果当前商品利润大于堆顶的,就弹出堆顶元素,并将其插入。

代码:

#include<stdio.h>
#include<algorithm>
#include<queue>
#include<vector>
#include<iostream>
using namespace std;
typedef pair<int,int>PII;
const int N = 1E4 + 10;

PII goods[N];
int n ;

int main(){
    while(~scanf("%d",&n)){
        for(int i = 0,x,y ; i < n ; ++i)
        {    
            
            scanf("%d%d",& x, &y);
            goods[i].first = y; // 因为pair 自带的是先以第一关键字排序,所以将时间放到
            goods[i].second = x;
        }
        
        sort(goods,goods + n);
        priority_queue<int,vector<int>,greater<int>>sale;
        for(int i = 0 ; i < n ;++i){
            int day = goods[i].first , value = goods[i].second;
            if(day > sale.size())
                sale.push(value);
            else if (day == sale.size() && value > sale.top()){
                sale.pop();
                sale.push(value);
            }
        }
        int res = 0;
        while(!sale.empty())
        {
            res += sale.top();
            sale.pop();
        }
        printf("%d\n",res);
    }
    
    return 0;
}

序列

题目链接

解题思路:

最暴力的做法当然是枚举和的所有可能,不过这数据范围必爆呀。那么我们可以思考一下我们是否可以先从前面两个序列里选出和最小的n个数,然后这n个数继续与后边的进行合并,一共合并m-1次。那么最关键的就是如何在两个序列中找出和最小的n个和。如下图,先对第一组进行排序,然后分组
在这里插入图片描述
在这里因为a是已经排好序的,所以每一组的大小关系都是从小到大的。每一组的第一个元素之中的最小值就是最小值。因此我们将每一组的第一个元素放入小跟堆里进行维护。在选出一个后删除,再添加的时候是添加该组里边的下一个位置。有以下技巧: s - a[p] + a[p+1]

代码:

#include<stdio.h>
#include<algorithm>
#include<queue>
#include<vector>

using namespace std;
typedef pair<int,int>PII;

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

void merge(){
    priority_queue<PII,vector<PII>,greater<PII>>heap;
    for(int i = 0  ;  i < n ; ++i)heap.push({b[i] + a[0],0});
    for(int i = 0 ; i < n ; ++i){
        auto t = heap.top();
        heap.pop();
        int s = t.first , p = t.second;
        c[i] = s;
        heap.push({s - a[p] + a[p+1] , p + 1});
    }
    for(int i = 0 ; i < n ; ++i)a[i] = c[i];
    
}

int main(){
    int T ;
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&m,&n);
        for(int i = 0 ; i < n ; ++i)scanf("%d",&a[i]);
    
        sort(a,a+n);
        
        for(int i = 0 ; i < m-1 ;++i){
            for(int j = 0 ; j < n ; ++j)scanf("%d",&b[j]);
            merge();
        }
        for(int i = 0 ; i <n ;++i)printf("%d ",a[i]);
        puts("");
    }
    return  0;
}

数据备份

题目链接

解题思路:
由于还没有证明出结论的正确性,先用上别的大佬的,
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
出处:https://www.acwing.com/solution/content/29786/

难点:

  1. 本题除了思路难想之外,实现也不好实现。第一个是我们在选了一个点之后要把其该点删除掉。而这就利用双向链表来处理。我们原本是要维护一个小根堆,但是在选完一个点之后要在堆里动态删除与其相邻的两个点,因此我们利用set、来实现,set中的元素也会默认排好序。

代码:

#include<stdio.h>
#include<algorithm>
#include<set>

using namespace std;
typedef long long LL;
typedef pair<LL,int>PLI;
const int N = 1E5 + 10;
int l[N] , r[N];
LL d[N];
int n , k ;

void delete_node(int x){
    r[l[x]] = r[x];
    l[r[x]] = l[x];
}

int main(){
    scanf("%d%d",&n,&k);
    for(int i = 0 ; i < n ; ++i)scanf("%lld",&d[i]);
    for(int i = n - 1 ; ~i ; --i)d[i] = d[i] - d[i-1];
    d[0] = d[n] = 1e15;
     set<PLI>heap;
    for(int i = 1 ; i < n ; ++i){
        l[i] = i - 1 , r[i] = i + 1;
        if( i >= 1 && i < n )heap.insert({d[i],i});
    }
    LL res = 0;
   while(k--){
       auto t  = heap.begin();
       LL v = t -> first ;
       int p = t -> second , left = l[p] , right = r[p];
       heap.erase(t);
       heap.erase({d[left],left}) , heap.erase({d[right],right});
       delete_node(left) , delete_node(right);
       res += v;
       
       d[p] = d[left] + d[right] - d[p];
       heap.insert({d[p],p});
   }
    printf("%lld\n",res);
    
    return 0;
}

合并果子 (二叉哈夫曼树)

题目链接

在这里插入图片描述
在这里插入图片描述

二叉哈夫曼树的创建步骤:

  1. 将节点插入小根堆中。
  2. 从堆中取出最小的两个权值w1和w2 ,令ans += w1 + w2 ;
  3. 建立一个权值为w1 + w2 的树节点p,令p成为权值w1和w2的树节点的父亲。
  4. 在堆中插入 w1 + w2 。
  5. 重复第2~4 步直到堆得大小为1;

代码:

#include<stdio.h>
#include<algorithm>
#include<queue>
#include<vector>

using namespace std;

const int N = 10010;
int n ;
int main(){
    scanf("%d",&n);
    priority_queue<int,vector<int>,greater<int>>fruits;
    for(int i = 0 ,x ; i < n; ++i){
        scanf("%d",&x);
        fruits.push(x);
    }
    int res = 0;
    while(fruits.size() > 1){
        int fruit1 = fruits.top(); fruits.pop();
        int fruit2 = fruits.top() ; fruits.pop();
        res += fruit1 + fruit2;
        fruits.push(fruit1 + fruit2);
    }
    printf("%d\n",res);
    return 0;
}

荷马史诗(k叉哈夫曼树)

题目链接

解题思路:
在这里插入图片描述

k叉哈夫曼树的创建步骤

  1. 将节点插入小根堆中。
  2. 如果 (n-) % (k-1 ) != 0 ,将0加入小根堆中。
  3. 建立一个权值为w1 + w2 + … + wk 的树节点p,令p成为权值w1、w2 … wk的树节点的父亲。同时记录深度为各个儿子节点深度的最大值。
  4. 在堆中插入 w1 + w2 ,与深度 depth + 1 .
  5. 重复第3~4 步直到堆得大小为1;

代码:

#include<stdio.h>
#include<algorithm>
#include<queue>
#include<vector>

using namespace std;

typedef long long ll;
typedef pair<ll,int>PLI;

int n , k;

int main(){
    scanf("%d%d",&n,&k);
    // 应为用了pair 会先以first 为第一关键字从小到大排好序
    //如果相同会以second 作为第二关键字从小到大排序
    //而这就恰好贪心处理了在权值相同的情况下优先选 节点深度(seocnd)小的节点
    priority_queue<PLI,vector<PLI>,greater<PLI>>heap;
    
    for(int i = 0  ; i < n ; ++i){
        ll x;
        scanf("%lld",&x);
        heap.push({x,0});
    }
    
    while((n-1)%(k-1)) heap.push({0,0}) , n++;
    
    ll res = 0;
    
    while(heap.size() > 1){
        ll s = 0;
        int depth = 0;
        for(int i = 0 ; i < k ; ++i){
            auto t = heap.top();
            heap.pop();
            s += t.first;
            depth = max(depth , t.second);
        }
        res += s;
        heap.push({s,depth + 1});
    }
    printf("%lld\n%d\n",res,heap.top().second);
    
    return 0 ;
}

小结

小知识点

  1. 当数据范围大于一百万的时候,利用scanf()来读取,会明显比cin好。

解题思路

一般来说先想一下用暴力什么做,之后看有那些数据结构或者算法可以对其进行优化。(如在暴力做的时候发现一些性质)

由数据范围反推算法复杂度以及算法内容

在这里插入图片描述
来自:https://www.acwing.com/blog/content/32/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落春只在无意间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值