算法基础2.1链表与邻接表,栈与队列,KMP

链表,栈与队列都有很多种实现方式,链表可以用指针加结构体的一个实现方式,栈和队列可以拿stl里面的容器来直接写,但这里讲的是如何用数组来模拟,有这么几个原因,首先第一个原因是因为效率问题,速度快,C++里面new的操作,动态分配一个新的节点,这个操作是非常慢的,在算法题里面,如果用new来生成新节点80%会超时,因此在算法题里面绝大部分时候都是用数组模拟链表。

今天主要是讲两种列表,第一个是用数组来模拟一个单链表,第二个拿数组来模拟一下双链表,单链表在算法题或者笔试里用的最多的是邻接表,邻接表其实是n个链表,邻接表的最主要的应用就是存储树和图。后面会讲到最短路问题,最小乘除数问题,最大流问题,都是拿邻接表来存的。双链表用的比较多的情况是用来优化某些问题。

单链表

单链表:有一个头结点head,然后头节点最一开始的时候指向一个空节点,然后每次会在头结点和空节点往里插入一个新的元素,每一个点都会有两个值,一个value和一个next指针,用数组来模拟,首先需要定义的是value,是一个数组,习惯上用e[N]来表示某个点的值是多少,用ne[N]来表示某个点的next指针是多少,e和ne是用下标来关联起来的,空结点的下标可以用负一来表示。

单链表是可以在任意位置插入的,但是如果想在O1的时间复杂度之内插入的话,就只能是在某一个点后面一个点插入。因为单链表有一个性质,从定义里面可以发现,单链表可以用Oe的时间直接找到下一个点的位置,但是找不到上一个点的位置,单链表是只往后看不往前看的,如果想找到一号点的前面一个点,只能从头开始遍历。

黄颜色的框框就是上面这个链表在数组里边的表达式,e和ne都是整数型的数组。

模板题:

实现一个单链表,链表初始为空,支持三种操作:

  1. 向链表头插入一个数;
  2. 删除第 k 个插入的数后面的一个数;//即删除下标是k-1点的后面一个数
  3. 在第 k个插入的数后插入一个数。

现在要对该链表进行 M次操作,进行完所有操作后,从头到尾输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k个数。例如操作过程中一共插入了 n个数,则按照插入的时间顺序,这 n个数依次为:第 1个插入的数,第 2 个插入的数,…第 n个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. H x,表示向链表头插入一个数 x。
  2. D k,表示删除第 k 个插入的数后面的数(当 k为 0 时,表示删除头结点)。
  3. I k x,表示在第 k 个插入的数后面插入一个 x(此操作中 k均大于 0)。
输出格式

共一行,将整个链表从头到尾输出。

数据范围

1≤M≤100000
所有操作保证合法。

输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5

// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 在链表头插入一个数a
void insert(int a)
{
    e[idx] = a, ne[idx] = head, head = idx ++ ;
}

// 将头结点删除,需要保证头结点存在
void remove()
{
    head = ne[head];
}

#include <iostream>

using namespace std;

const int N = 100010;


// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 将x插到头结点,第一步把x的next指针指向head节点指针指向的点,第二步head的指针删掉,然后指向x,
void add_to_head(int x)
{
    e[idx] = x,  //把x这个值存下来

ne[idx] = head,  //x指向head指向的值

head = idx ;  //head指向x

idx++,  //idx已经用过了,指向下一个位置
}

// 将x插到下标是k的点后面
void add(int k, int x)
{
    e[idx] = x,   //存值

ne[idx] = ne[k],  //x指向k的下一个点

ne[k] = idx

idx++ ;
}

// 将下标是k的点后面的点删掉
void remove(int k)
{
    ne[k] = ne[ne[k]];  //让k的指针直接指向k后面元素指针指向的位置
}

int main()
{
    int m;
    cin >> m;

    init();

    while (m -- )
    {
        int k, x;
        char op;

        cin >> op;
        if (op == 'H')
        {
            cin >> x;
            add_to_head(x);
        }
        else if (op == 'D')
        {
            cin >> k;
            if (!k) head = ne[head];  //若k=0删除头结点,让它直接指向它指向点的下一个点,它指向的是head本身,下一个就套层next
            else remove(k - 1);
        }
        else
        {
            cin >> k >> x;
            add(k - 1, x);    //注意k-1,0号是第一个插入的点
        }
    }

    for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';   //遍历一遍,不包括空结点
    cout << endl;

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/42977/
来源:AcWing

双链表

双链表跟单链表类似,单链表是每一个节点只有一个指向后面的指针,双链表就是每一个节点会有两个指针,一个指向前,另外一个指向后,因此在定义的时候,定义l[N]存的是每个点左边的指针,r[N]存的是每个点右边的指针,让下边是零的点,是head,是最左边的点,然后下标是一的点,是tail,是最右边的点。

// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    //0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

作者:yxc
链接:https://www.acwing.com/blog/content/404/
来源:AcWing

模板题:

实现一个双链表,双链表初始为空,支持 5种操作:

  1. 在最左侧插入一个数;
  2. 在最右侧插入一个数;
  3. 将第 k 个插入的数删除;
  4. 在第 k个插入的数左侧插入一个数;
  5. 在第 k个插入的数右侧插入一个数

现在要对该链表进行 M次操作,进行完所有操作后,从左到右输出整个链表。

注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1个插入的数,第 2个插入的数,......第 n个插入的数。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

  1. L x,表示在链表的最左端插入数 x。
  2. R x,表示在链表的最右端插入数 x。
  3. D k,表示将第 k个插入的数删除。
  4. IL k x,表示在第 k个插入的数左侧插入一个数。
  5. IR k x,表示在第 k个插入的数右侧插入一个数。
输出格式

共一行,将整个链表从左到右输出。

数据范围

1≤M≤100000
所有操作保证合法。

输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9


#include <iostream>

using namespace std;

const int N = 100010;

int m;
int e[N], l[N], r[N], idx;

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;   //赋x值
    l[idx] = a,   //指左指针

   r[idx] = r[a];  //指右指针
    l[r[a]] = idx,  //右点的左指针指向x

     r[a] = idx  //a的右指针指向x

     idx++ ;
}   //如果想在左边插入可以直接调用add(l[k],x),就是在k的左边这个点的右边插入x和在k的左边插入是一样的,因此,双链表的插入只用实现一个就可以了。

// 删除节点a
void remove(int a)
{
    l[r[a]] = l[a];   //
    r[l[a]] = r[a];
}

int main()
{
    cin >> m;

    // 0是左端点,1是右端点,初始化
    r[0] = 1, l[1] = 0;
    idx = 2;  //0和1已经被占用了

    while (m -- )
    {
        string op;
        cin >> op;
        int k, x;
        if (op == "L")
        {
            cin >> x;
            insert(0, x);
        }
        else if (op == "R")
        {
            cin >> x;
            insert(l[1], x);
        }
        else if (op == "D")
        {
            cin >> k;
            remove(k + 1);
        }
        else if (op == "IL")
        {
            cin >> k >> x;
            insert(l[k + 1], x);
        }
        else
        {
            cin >> k >> x;
            insert(k + 1, x);
        }
    }

    for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
    cout << endl;

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/42982/
来源:AcWing
 

邻接表

邻接表就是把每个点的所有邻边全部存下来,就相当于开了n个单链表,所有的邻边,然后用一个链表来存起来了。

栈是先进后出,队列是先进先出。

 栈    // tt表示栈顶
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;

// 栈顶的值
stk[tt];

// 判断栈是否为空,如果 tt > 0,则表示不为空
if (tt > 0)
{

}

栈模板题:

实现一个栈,栈初始为空,支持四种操作:

  1. push x – 向栈顶插入一个数 x;
  2. pop – 从栈顶弹出一个数;
  3. empty – 判断栈是否为空;
  4. query – 查询栈顶元素。

现在要对栈进行 M 个操作,其中的每个操作 3和操作 4都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式

对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YES 或 NOquery 操作的查询结果为一个整数,表示栈顶元素的值。

数据范围

1≤M≤100000,
1≤x≤10的9次方
所有操作保证合法。

输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO

include <iostream>

using namespace std;

const int N = 100010;

int m;
int stk[N], tt;  //stk是栈,tt是栈顶

int main()
{
    cin >> m;
    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            cin >> x;
            stk[ ++ tt] = x;  //插入
        }
        else if (op == "pop") tt -- ;   //弹出 
        else if (op == "empty") cout << (tt ? "NO" : "YES") << endl;  //判断栈是否为空,大于0不空
        else cout << stk[tt] << endl;   //取出栈顶
    }

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/43103/
来源:AcWing

模板题:表达式求值

给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

  • 数据保证给定的表达式合法。
  • 题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。
  • 题目保证表达式中所有数字均为正整数。
  • 题目保证表达式在中间计算过程以及结果中,均不超过 2的31次方-1。
  • 题目中的整除是指向 0 取整,也就是说对于大于 0 的结果向下取整,例如 5/3=1,对于小于 0 的结果向上取整,例如 5/(1−4)=−1。
  • C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。
输入格式

共一行,为给定表达式。

输出格式

共一行,为表达式的结果。

数据范围

表达式的长度不超过 105105。

输入样例:
(2+2)*(1+1)
输出样例:
8

思考思路:

先看下只有 + 和 * 的。

输入长度为n的字符串,例如:1+2+3*4*5

输出表达式的值,即:63

应该用什么数据结构?

栈。

应该先计算哪一步?

实际应该先计算1+2。

“表达式求值”问题,两个核心关键点:

(1)双栈,一个操作数栈,一个运算符栈;

(2)运算符优先级,栈顶运算符,和,即将入栈的运算符的优先级比较:

如果栈顶的运算符优先级低,新运算符直接入栈

如果栈顶的运算符优先级高,先出栈计算,新运算符再入栈

仍以1+2+3*4*5举例,看是如何利用上述两个关键点实施计算的。

首先,这个例子只有+和*两个运算符,所以它的运算符表是: 

这里的含义是:

(1)如果栈顶是+,即将入栈的是+,栈顶优先级高,需要先计算,再入栈;

(2)如果栈顶是+,即将入栈的是*,栈顶优先级低,直接入栈;

(3)如果栈顶是*,即将入栈的是+,栈顶优先级高,需要先计算,再入栈;

(4)如果栈顶是*,即将入栈的是*,栈顶优先级高,需要先计算,再入栈;

有了运算符表,一切就好办了。

一开始,初始化好输入的字符串,以及操作数栈,运算符栈。

一步步,扫描字符串,操作数一个个入栈,运算符也入栈。

下一个操作符要入栈时,需要先比较优先级。

栈内的优先级高,必须先计算,才能入栈。


计算的过程为:

(1)操作数出栈,作为num2;

(2)操作数出栈,作为num1;

(3)运算符出栈,作为op;

(4)计算出结果;

(5)结果入操作数栈;

接下来,运算符和操作数才能继续入栈。下一个操作符要入栈时,继续比较与栈顶的优先级。

栈内的优先级低,可以直接入栈。

字符串继续移动。

又要比较优先级了。

栈内的优先级高,还是先计算(3*4=12),再入栈。

不断入栈,直到字符串扫描完毕。


不断出栈,直到得到最终结果3+60=63,算法完成。

总结

“表达式求值”问题,两个核心关键点:

(1)双栈,一个操作数栈,一个运算符栈;

(2)运算符优先级,栈顶运算符,和,即将入栈的运算符的优先级比较:
如果栈顶的运算符优先级低,新运算符直接入栈

如果栈顶的运算符优先级高,先出栈计算,新运算符再入栈

这个方法的时间复杂度为O(n),整个字符串只需要扫描一遍。

运算符有+-*/()~^&都没问题,如果共有n个运算符,会有一个n*n的优先级表。

代码:

#include <iostream>
#include <stack>
#include <string>
#include <unordered_map>
using namespace std;

stack<int> num;
stack<char> op;

//优先级表
unordered_map<char, int> h{ {'+', 1}, {'-', 1}, {'*',2}, {'/', 2} };


void eval()//求值
{
    int a = num.top();//第二个操作数
    num.pop();

    int b = num.top();//第一个操作数
    num.pop();

    char p = op.top();//运算符
    op.pop();

    int r = 0;//结果 

    //计算结果
    if (p == '+') r = b + a;
    if (p == '-') r = b - a;
    if (p == '*') r = b * a;
    if (p == '/') r = b / a;

    num.push(r);//结果入栈
}

int main()
{
    string s;//读入表达式
    cin >> s;

    for (int i = 0; i < s.size(); i++)
    {
        if (isdigit(s[i]))//数字入栈
        {
            int x = 0, j = i;//计算数字
            while (j < s.size() && isdigit(s[j]))
            {
                x = x * 10 + s[j] - '0';
                j++;
            }
            num.push(x);//数字入栈
            i = j - 1;
        }
        //左括号无优先级,直接入栈
        else if (s[i] == '(')//左括号入栈
        {
            op.push(s[i]);
        }
        //括号特殊,遇到左括号直接入栈,遇到右括号计算括号里面的
        else if (s[i] == ')')//右括号
        {
            while(op.top() != '(')//一直计算到左括号
                eval();
            op.pop();//左括号出栈
        }
        else
        {
            while (op.size() && h[op.top()] >= h[s[i]])//待入栈运算符优先级低,则先计算
                eval();
            op.push(s[i]);//操作符入栈
        }
    }
    while (op.size()) eval();//剩余的进行计算
    cout << num.top() << endl;//输出结果
    return 0;
}


作者:Hasity
链接:https://www.acwing.com/solution/content/40978/
来源:AcWing

队列

1.普通队列

// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

// 判断队列是否为空,如果 hh <= tt,则表示不为空
if (hh <= tt)
{

}

2. 循环队列
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空,如果hh != tt,则表示不为空
if (hh != tt)
{

}

模板题:

实现一个队列,队列初始为空,支持四种操作:

  1. push x – 向队尾插入一个数 x;
  2. pop – 从队头弹出一个数;
  3. empty – 判断队列是否为空;
  4. query – 查询队头元素。

现在要对队列进行 M个操作,其中的每个操作 3和操作 4都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push xpopemptyquery 中的一种。

输出格式

对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YES 或 NOquery 操作的查询结果为一个整数,表示队头元素的值。

数据范围

1≤M≤100000,
1≤x≤10的9次方,
所有操作保证合法。

输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4

#include <iostream>

using namespace std;

const int N = 100010;

int m;
int q[N], hh, tt = -1;  //hh是队头,tt是队尾

int main()
{
    cin >> m;

    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            cin >> x;
            q[ ++ tt] = x;  //在队尾插入元素
        }
        else if (op == "pop") hh ++ ;  //在队头弹出元素
        else if (op == "empty") cout << (hh <= tt ? "NO" : "YES") << endl;  //判断是否为空,hh<=tt为空
        else cout << q[hh] << endl;   //取出队头元素
    }

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/43104/
来源:AcWing

单调栈

单调栈和单调队列虽然很抽象,但能用的题型非常的少。

单调栈最常见的一个模型是什么?就是给定一个序列,求一下这个序列当中的每一个数左边离它最近的且比它小的数在什么地方,或者对称

例:34275,找到每一个数,左边离它最近的且比它小的数,如果不存在返回负一。那么三的话,不存在,返回负一,四的话是左边离它最近比它小的数是三,返回三,二的话,左边所有数都比它大,因此返回负一。啊返回负一,七的话,左边离它最近的比它小的数是二,返回二,五的话,左边离它最近的比它小的是二,因此返回二,考虑的方式其实和双指针是类似的,先想一下暴力做法是什么,然后再挖掘一些性质,可以把目光集中在比较少的状态里面,从而起到把一个问题的时间复杂度降低这样一个效果。首先先想一下暴力做法是什么,

然后看一下暴力做法里面有没有什么性质,可以发现,随着i往右走的过程当中,可以用一个栈来存储i左边的所有元素,最开始栈是空的,然后i指针每往右边移动一个位置,就往这个栈里面加入一个新的数,栈里面存的是a1,a2,一直到ai- 1,就是把i左边所有数全部存到里面去。然后每一次找的时候,从栈点开始往后找,找到第一个比i小的数为止,这个是一个暴力做法,然后来看一下有没有什么性质,主要目光是放到栈里面去,看一下栈里边是不是有些元素永远不会作为答案输出来,举个例子,假设在这个栈里边a3大于a5,那么a3是不是永远不会作为答案输出来?因为a5是在a3的右边,而且a5比a3小或者相等,因此当我们找的时候,如果说a3是我们的目标值的话,那么我们一定可以换成a5会更好,如果a3是我们的目标值的话,那么就意味着a3是小于ai的,但是由于a5是小于等于a3的,因此a5也小于ai,而且我们要找的时候肯定找的时候是找离i最近的一个比i小的数,因此a3是一定不会被用到的,因此如果这个栈里边存在这样的关系,比方说存在一个ax大于等于ay,并且x小于y,那么ax可以被删掉,因此只要有这样逆序的关系,前面这个数就会被删掉。所以最后剩下的这个序列就一定是一个单调序列了。这个是单调的一个基本的思路。

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

模板题:

给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

输入格式

第一行包含整数 N,表示数列长度。

第二行包含 N 个整数,表示整数数列。

输出格式

共一行,包含 N 个整数,其中第 i个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。

数据范围

1≤N≤10的5次方
1≤数列中元素≤10的9次方

输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2

#include <iostream>

using namespace std;

const int N = 100010;

int stk[N], tt;

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        while (tt && stk[tt] >= x) tt -- ;  //栈顶空并且栈上元素大于x,栈顶元素就永远不会被用到了,tt--
        if (!tt) printf("-1 ");  //空栈输出-1
        else printf("%d ", stk[tt]);
        stk[ ++ tt] = x;  //把x插到栈里面去
    }

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/43105/
来源:AcWing

时间复杂度,虽然有两重循环,但是关注一下循环的下标tt,然后可以发现每一个元素,它只会进栈一次而且每个元素最多只会出栈一次。因此每一个元素只会进栈一次,出栈一次,总共加到一块也只有2n次操作,所以整个算法的时间复杂度就是On。

单调队列

最经典的一个应用就是求一下滑动窗口里面的最大值或者最小值。

举个例子,1,3,-1,-3,5,3,6,7,在所有长度是三的滑动窗口里面的最大值和最小值输出来。先是-1,然后窗口往后移动一位,新的窗口里面的最小值是-3,以此类推

思路和单调栈优化是一样的,就是先想一下暴力怎么做,然后把其中没有用的元素删掉,就可以得到单调性了,然后有单调性的话,再去求极值。就可以直接拿第一个点或者最后一个点,就可以把这个本来要枚举一遍的时间复杂度变成O1了。首先这个窗口的话是可以用队列来维护的,比方说这个窗口的一开始长度是三,先从空开始移动,第一次先把一个数拿进来,其实就是在队尾里面插入一个一,然后第二次再在队尾插入一个三,第三次在队尾里面插入一个负一,然后第四次是两步,首先先把新的负三先插进来,第二步把一从队头弹出去。保证队列里边时时刻刻存的都是当前窗口里边的所有元素。那每一次移动的时候分两步来操作:第一步是把新元素插到队尾,然后第二步是把划出去的元素从队尾弹出来。所以说可以用一个队列来维护窗口,每一次求极值的时候,暴力做法就是直接去遍历一下队列里边的所有元素,时间复杂度是Ok,最后一共有n步,所以总共的时间复杂度就是O(nk),那么考虑一下怎么去优化这个问题,优化和单调栈问题是类似的,就是看一下这里边是不是有些元素是没有用的,然后把这些没有用的元素删掉的话,看一下会不会得到单调性,比方说以窗口3 -1 -3为例,首先当负三进来之后,第一个三一定没有用,每次实际上是求队列里面的最小值,因为负三是小于第一个三的,而且第一个三在负三的左边,所以说这个三会被先弹出去。这个负一也是一样,因为这个负一是在负三的左边,所以说这个负三会在负一后面被弹出去。那么由于负三小于等于负一,所以这个负一也是不会被当做最小值输出的,它也是没有用的,那么也把它删掉。因此只要有这样逆序对的话,就可以把这个大的点删掉啊,把所有这样逆序对全部删掉,整个数列就一定会变成一个严格单调上升的数列了,一个严格单调上升的队列的最小值应该是队头q [hh]。所以来总结一下单调栈和单调队列的问题,其实做法都是一样的,就是先考虑用栈和队列来暴力的模拟原来这个问题,先把朴素算法想清楚,然后再看一下在朴素算法里面栈和队列里边哪些元素是没有用的,然后把这些所有没有用的元素全部删掉,再看一下是不是有单调性,如果剩下的元素有单调性的话,就可以做优化了,取最值可以直接取两个端点,如果找一个值的话,可以用二分。

模板题:

给定一个大小为 n≤10的6次方 的数组。

有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 k 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k为 3。

窗口位置最小值最大值
[1 3 -1] -3 5 3 6 7-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 [-3 5 3] 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 5 [3 6 7]37

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 n和 k,分别代表数组长度和滑动窗口的长度。

第二行有 n个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7

常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}
 

include <iostream>

using namespace std;

const int N = 1000010;

int a[N], q[N];

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

    int hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ )
    {
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;  //判断队头是否已经滑出窗口,起点是i-k+1

        while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;  //q存的是下标,如果新插入的数比队尾的数要小的话,队尾的数就没有用了,把队尾删掉
        q[ ++ tt] = i;  //先加进去,i有可能是最小值

        if (i >= k - 1) printf("%d ", a[q[hh]]);  //从前k个数开始输出的,当不足k个就不用输出了
    }

    puts("");
    hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ )
    {
        if (hh <= tt && i - k + 1 > q[hh]) 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;
}

    return 0;

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/43107/
来源:AcWing

KMP

首先,什么是KMP算法。这是一个字符串匹配算法,对暴力的那种一一比对的方法进行了优化,使时间复杂度大大降低(KMP是取的三个发明人的名字首字母组成的名字)。

kmp的思考方式和一般的单调栈和单调队列和双指针算法是类似的,就是先想一下暴力怎么做,然后想一下如何去优化,

假设s是要匹配的串,是比较长的串,p是模式串,是比较短的串。首先枚举下标,kmp一般习惯上下标是从一开始的。

假设蓝颜色表示的是s,比较长的这个串。红颜色的表示的是模板串p,首先从第一个位置开始匹配

找到不同的元素后,停止之后模板串往后移动一位。然后再去重新匹配,这是暴力做法的一个想法,可以发现,由于已经匹配这么多了,所以其实是有一些额外信息在里面的。如果可以利用这些额外信息,就可以帮助我们少枚举一些东西。

基本概念:

1、s[ ]是模板串,即比较长的字符串。
2、p[ ]是模式串,即比较短的字符串。(这样可能不严谨。。。)
3、“非平凡前缀”:指除了最后一个字符以外,一个字符串的全部头部组合。
4、“非平凡后缀”:指除了第一个字符以外,一个字符串的全部尾部组合。(后面会有例子,均简称为前/后缀)
5、“部分匹配值”:前缀和后缀的最长共有元素的长度。
6、next[ ]是“部分匹配值表”,即next数组,它存储的是每一个下标对应的“部分匹配值”,是KMP算法的核心。(后面作详细讲解)。

核心思想:在每次匹配时,不是把p串往后移一位,而是把p串往后移动至下一次可以和前面部分匹配的位置,这样就可以跳过大多数的匹配步骤。而每次p串移动的步数就是通过查找next[ ]数组确定的。

next数组的含义及手动模拟(具体求法和代码在后面)
​ 然后来说明一下next数组的含义:对next[ j ] ,是p[ 1, j ]串中前缀和后缀相同的最大长度(部分匹配值),即 p[ 1, next[ j ] ] = p[ j - next[ j ] + 1, j ]。

如:

手动模拟求next数组:

对 p = “abcab”

  p        a    b    c    a    b
下标    1    2    3    4    5
next[ ]    0    0    0    1    2
对next[ 1 ] :前缀 = 空集—————后缀 = 空集—————next[ 1 ] = 0;

对next[ 2 ] :前缀 = { a }—————后缀 = { b }—————next[ 2 ] = 0;

对next[ 3 ] :前缀 = { a , ab }—————后缀 = { c , bc}—————next[ 3 ] = 0;

对next[ 4 ] :前缀 = { a , ab , abc }—————后缀 = { a . ca , bca }—————next[ 4 ] = 1;

对next[ 5 ] :前缀 = { a , ab , abc , abca }————后缀 = { b , ab , cab , bcab}————next[ 5]=2;

匹配思路和实现代码
​ KMP主要分两步:求next数组、匹配字符串。个人觉得匹配操作容易懂一些,疑惑我一整天的是求next数组的思想。所以先把匹配字符串讲一下。

​ s串 和 p串都是从1开始的。i 从1开始,j 从0开始,每次s[ i ] 和p[ j + 1 ]比较

当匹配过程到上图所示时,

s[ a , b ] = p[ 1, j ] && s[ i ] != p[ j + 1 ] 此时要移动p串(不是移动1格,而是直接移动到下次能匹配的位置)

其中1串为[ 1, next[ j ] ],3串为[ j - next[ j ] + 1 , j ]。由匹配可知 1串等于3串,3串等于2串。所以直接移动p串使1到3的位置即可。这个操作可由j = next[ j ]直接完成。 如此往复下去,当 j == m时匹配成功。

代码如下

for(int i = 1, j = 0; i <= n; i++)
{
    while(j && s[i] != p[j+1]) j = ne[j];
    //如果j有对应p串的元素, 且s[i] != p[j+1], 则失配, 移动p串
    //用while是由于移动后可能仍然失配,所以要继续移动直到匹配或整个p串移到后面(j = 0)

    if(s[i] == p[j+1]) j++;
    //当前元素匹配,j移向p串下一位
    if(j == m)
    {
        //匹配成功,进行相关操作
        j = next[j];  //继续匹配下一个子串
    }
}
注:采用上述的匹配方法( i 与 j+1 比较)我不清楚(其实是想不清楚)为什么要这样。。。脑子有点不好使。而不推荐下标从0开始的原因我认为是:若下标从0开始的话,next[ ]数组的值都会相应-1,这就会导致它的实际含义与其定义的意思不符(部分匹配值和next数组值相差1),思维上有点违和,容易出错。
(在实际操作上下标从0开始代码会多很多东西,比从1开始复杂一些,嗯。。。确实

求next数组的思路和实现代码
​ next数组的求法是通过模板串自己与自己进行匹配操作得出来的(代码和匹配操作几乎一样)。

代码如下:

for(int i = 2, j = 0; i <= m; i++)
{
    while(j && p[i] != p[j+1]) j = next[j];

    if(p[i] == p[j+1]) j++;

    next[i] = j;
}
代码和匹配操作的代码几乎一样,关键在于每次移动 i 前,将 i 前面已经匹配的长度记录到next数组中。

小模板:// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}

// 注:这不是模板题的AC代码,是一个最基本的模板代码
#include <iostream>

using namespace std;

const int N = 100010, M = 10010; //N为模式串长度,M匹配串长度

int n, m;
int ne[M]; //next[]数组,避免和头文件next冲突
char s[N], p[M];  //s为模版串, p为匹配串

int main()
{
    cin >> n >> s+1 >> m >> p+1;  //下标从1开始

    //求next[]数组
    for(int i = 2, j = 0; i <= m; i++)
    {
        while(j && p[i] != p[j+1]) j = ne[j];
        if(p[i] == p[j+1]) j++;
        ne[i] = j;
    }
    //匹配操作
    for(int i = 1, j = 0; i <= n; i++)
    {
        while(j && s[i] != p[j+1]) j = ne[j];
        if(s[i] == p[j+1]) j++;
        if(j == m)  //满足匹配条件,打印开头下标, 从0开始
        {
            //匹配完成后的具体操作
            //如:输出以0开始的匹配子串的首字母下标
            //printf("%d ", i - m); (若从1开始,加1)
            j = ne[j];            //再次继续匹配
        }
    }

    return 0;
}

模板题:

给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模式串 P 在字符串 S 中多次作为子串出现。

求出模式串 P在字符串 S 中所有出现的位置的起始下标。

输入格式

第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S 的长度。

第四行输入字符串 S。

输出格式

共一行,输出所有出现位置的起始下标(下标从 00 开始计数),整数之间用空格隔开。

数据范围

1≤N≤≤10的5次方
1≤M≤10的6次方

输入样例:
3
aba
5
ababa
输出样例:
0 2

//KMP
#include<iostream>
#include<cstdio>
using namespace std;
const int N=100010,M=1000010;
int n,m,ne[N];char s[M],p[N];
int main()
{
    cin>>n>>p+1>>m>>s+1;//s中 模板串p 求p作为子串在s中的起始下标们

    for(int i=2,j=0;i<=n;i++)  //求next数组 
    { //对于next数组,可以理解为比较s与p时,发现了不同,找到此时模板链中<以1为起点,到不同处(j)的一段字符串中>最长相同前后缀中的 后缀 的第一个字母下标  
      //如..ababac...与ababab比较时,到c处发现不同,在模板链中<ababa(b,但b与c不同)>的最长相同前后缀,即aba,
      //第二个aba的下标为3,即next[5]=3,即下次查找时直接从目前点向后跳3个字儿 
        while(j&&p[i]!=p[j+1]) j=ne[j];   //j从零开始时p[2]与p[1]不考虑不相同

        if(p[i]==p[j+1]) j++;   //但如果p[2]真的等于p[1]
        ne[i]=j;                 //next[2]等于1,跳转到 ***处 
    }                            //如果p[2]不等于p[1],next[2]=0,去下一次循环
                                //此时第一句仍不满足,执行第二句 
                                //p[3]与p[1]比较,***直到p[x]与p[1]吻合,next[x]更新为1,j开始向后 
                                //开始第一句的判断,p[++x]与p[2]吻合,则next[x]更新2,j接着向后加 
                            //不吻合,则j去到以j为下标的next值,相当于一个与自身比较的过程接续下去; 
    for(int i=1,j=0;i<=m;i++)
    {
        while(j&&s[i]!=p[j+1]) j=ne[j]; //判断失败时,回到next数组对应值 
        if(s[i]==p[j+1]) j++;
        if(j==n)
        {
            printf("%d ",i-n);//p作为完整子串出现了,输出起始下标; 
            j=ne[j];    //续航 
        }
    }   
    return 0; 
}

end——————————————————————————————————————-———

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值