一个红题带你了解绿(黄)题知识点

2018-10-29 21:47:13


滑稽题解第一弹

大家好,我是一个入门一年还在入门的蒟蒻。

今天天气不错,我决定回新手村看看。

然后我就随手点开了一个题。

看到题面一开始我有点惊……现在新手村就开始用栈了吗?

然而事后我发现这个题好像并没有想象中的那么难……

于是我就尝试性地交了一个学了一年后的入门水平代码……

#include<iostream>
#include<stack>
using namespace std;
stack<int> a;
int k;
int main(){
    while(cin>>k) a.push(k);a.pop();//这种输入方式在本地调试的时候需要在输入结束后按Ctrl+Z来结束输入
    while(!a.empty()) cout<<a.top()<<" ",a.pop();
    return 0;
}

然后我们机房的c姓神犇就来指点了一下,然后诊断出我患有重度STL依赖症……

我看了一下我之前的代码:

#include<iostream>
using namespace std; 
int x[100],c=0;
int main(){
    for(int i=0;;i++){
        cin>>x[i];
        if(x[i]==0) break; 
        c=i;
    }
    for(int j=c;j>=0;j--)
    cout<<x[j]<<" ";
    return 0;
}

然后我就突发奇想,这个题是不是可以一题多解呢?

我觉得可以的。

那下面我就给大家讲一下我目前想到的一些方法,如有不足欢迎指正。

(以下代码均经过测试,大家可以放心食用)

(大部分知识点不会细讲,有兴趣的同学可以深入了解一下)


1.常规做法

这个做法还是比较简单的,就是把输入的数都存起来,然后反向遍历输出就好。代码就是我一年前的第一版,如下:

#include<iostream>
using namespace std; 
int x[100],c=0;
int main(){
    for(int i=0;;i++){
        cin>>x[i];
        if(x[i]==0) break; 
        c=i;
    }
    for(int j=c;j>=0;j--)
    cout<<x[j]<<" ";
    return 0;
}

2.栈

栈是NOIP里的一种必会的基础数据结构,结构简单功能强大(?)。栈的基本思想是先进后出,后进先出。利用栈的这个特性往往可以完成一些意想不到的操作。栈的一个用途就是将一串数据反向输出。

(1)手打栈

栈的实现比较简单,只需要开一个空数组,然后用一个top变量表示栈顶元素的位置即可。入栈就将top++然后将栈顶元素赋值即可。出栈只需top--,连清零都不用(善于思考的同学们可以想一下这是为什么)。下面就是一段手打栈代码:

#include<iostream>
using namespace std;
int a[101];//如果你的第一个数存储在a[1]里,一定要多开一两个空间 ,以防越界访问
int top=0,c;
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        a[++top]=c;
        /*
        或者写成:
        top++;
        a[top]=c;
        个人比较喜欢压码……
        */ 
    }
    while(top!=0){
        cout<<a[top--]<<" ";
        /*
        或者写成:
        cout<<a[top];
        top--;
        */ 
    }
    return 0;
} 

(2)STL栈

要问C++最大的好处是什么?那就是C++有着丰富的STL库(有兴趣的同学可以去百度一下STL的意思)。STL和普通数组的最大区别我认为体现在两方面:一是有着丰富的自带函数,可以省去手打的麻烦,二就是完全不用担心数组开的不够大或者过大的尴尬。你让它多大它就多大,空间零浪费。

STL中最基本的一种数据结构就是vector了。下面就是一版用vector写出来的代码:

#include<iostream>
#include<vector>//STL vector的头文件
using namespace std;
vector<int> a;//定义一个int型的vector 
int c;//STL可以完全不用担心数组大小的问题,这个和string类似 
int main(){
    while(1){//有时候也可以巧用死循环 
        cin>>c;
        if(c==0) break;//终止条件 
        a.push_back(c);//将括号里的元素压入vector尾部 
    }
    while(!a.empty()){
        cout<<a.back()<<" ";//.back()是一个返回vector尾部元素的函数 
        a.pop_back();//删除vector尾部的元素
    }
    /*
    这一部分输出程序也可以写成:
    for(int i=a.size()-1;i>=0;i--){//a.size()返回a中元素的个数
        cout<<a[i]<<" ";
    }
    要注意vector是从a[0]开始存储a.size()个元素,要当心越界访问
    */
    return 0;
} 

STL中还有一个专门为栈设计的数据结构——stack。它的操作与vector类似,但是函数名称简洁了不少,而且省去了一些对于栈无用的冗杂的函数。下面就是一版用stack写的代码:

#include<iostream>
#include<stack>//STL stack的头文件
using namespace std;
stack<int> a;//定义一个int型的stack 
int c;
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        a.push(c);//将括号里的元素压入stack顶部 
    }
    while(!a.empty()){
        cout<<a.top()<<" ";//.top()是一个返回stack顶部元素的函数 
        a.pop();//删除stack顶部的元素 
    }
    return 0;
} 

3.指针法

指针应该算是C++的特色之一了吧,指针的应用非常多而且非常灵活,建议大家都来学一学。尽管指针我从学过到现在都没用过几次,只有在变量名称实在太乱(比如 $a[b[t.m][2]+c[i]]$ )的时候才会用指针把它变成一个 $^*t$ 。

(1)数组指针

对于数组而言,如果你定义了一个数组 $a[...]$ ,那么如果你在程序中只写一个 $a$ 的话,系统就会默认这是指数组 $a$ 的第一个元素的地址。而如果你写 $a+i$ 的话,系统就会默认这是数组的第 $i$ 个元素也就是 $a[i-1]$ 的地址。下面就是一段数组指针写法的代码:

#include<iostream>
using namespace std;
int a[101],c;
int *p=a;//定义一个int类型的指针,指向数组不用取地址符 
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        p++;//指针指向当前所指元素在数组中的下一个元素 
        *p=c;//星号表示这个指针现在不是代表地址而是代表该地址的元素 
    }
    while(p!=a){//当指针不是指向数组的第一个元素地址 
        cout<<*p<<" ";
        p--;
    }
    return 0;
} 

(2)STL指针

指针在STL里有一个更高端大气上档次的名字——迭代器。迭代器这一块我也不是很懂,目前只有用 $.erase()$ 的时候才会用……有兴趣的话大家可以去了解一下。迭代器的用法和数组指针操作差不多,只是初始元素指针一般用STL自带的 $.begin()$ 函数返回。下面是用迭代器的代码:

#include<iostream>
#include<vector>
using namespace std;
vector<int> a;
int c;
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        a.push_back(c);
    }
    for(vector<int>::iterator it=a.end()-1;it>=a.begin();it--){
        /*a.end()返回的是a中最后一个元素后面的地址,因此一定要记得-1
        a.begin()返回a的第一个元素的地址 
        */ 
        cout<<*it<<" "; //*it表示it所指的元素 
    }
    return 0;
}

以下部分纯属科普,与正解关系不大,请大家酌情观看


4.链表

链表是一种很常用的数据结构,大名鼎鼎的邻接表用的就是链表来实现。对于本题,我们可以建立一个双向链表,将各个元素正向连接起来后反向遍历输出。代码如下:

#include<iostream>
using namespace std;
struct node{//定义链表的结构类型,叫什么其实无所谓 
    int pre;//表示指向它的前一个链表单元的下标,也可以用指针存储
    int next;//表示它指向的下一个链表单元的下标,也可以用指针存储
    int value;//存储它的赋值 
}a[101];//结构体方面的知识就不讲了,不会的同学可以补一下 
int c;
int n=0;//表示当前建立了几个链表单元
int re=0;//表示上一个链表单元 
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        n++;
        a[n].value=c;
        a[n].pre=re;
        a[re].next=n;
        re=n;
    }
    int now=n;//表示现在在访问第几个链表单元 
    while(now!=0){
        cout<<a[now].value<<" ";
        now=a[now].pre;
    }
    return 0;
}

5.排序

排序是一个合格oier必会的知识点之一。排序的方法很多,有桶排序,冒泡排序,插入排序,归并排序,快速排序等。这里我们先只讲两种排序:sort函数排序和STL优先队列堆排序。

(1)sort排序

这个题目里,我们可以用一个结构体来存储各个数据点被输入的先后顺序,然后按照从晚到早的顺序排一遍序,之后顺次输出即可。要注意的一点是,sort函数默认只能将存储数字类型的数组从小到大排序,如果要自定义排序优先级,则要自定义比较函数。函数需要两个参量,分别表示数组中被比较的两个元素。比较的规则是使排序后的两个元素能够满足在前的元素与在后的元素分别代入比较函数的前后两个参量后能够 $return \ 1$ 。直白点就是比较两个元素,满足条件自定义函数里的排在前面。代码如下:

#include<iostream>
#include<algorithm>//sort函数的头文件
using namespace std;
struct node{
    int time;//存储该数据被输入的时间先后
    int value;//存储该数据的赋值 
}a[101];
int c,n;
int cmp(node x,node y){//自定义比较函数 
    return x.time>y.time;
}
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        a[n].value=c;
        a[n].time=n;
        n++;
    }
    sort(a,a+n,cmp);//前两个变量分别表示开始排序的元素的地址和结束排序的元素的地址 
    for(int i=0;i<n;i++){
        cout<<a[i].value<<" ";
    }
    return 0;
} 

(2)STL优先队列堆排序

堆是一种功能强大(这个确实强)但是也非常难写的数据结构(有兴趣的同学可以去了解一下堆的写法和原理,这里就不再细讲了)。它的功能主要是将输入的数据以 $O(log \ n)$ 的时间复杂度排序。

但是大家不会写也不要紧,因为STL已经给大家准备好了堆的模板:优先队列——priority_queue。优先队列排序写法和sort类似,但是它的时间复杂度更优,而且不用调用函数就会随时对数组中的元素排序。不过它也有一些缺点,比如说不能对有序序列单点查询,只能每次访问排序后的第一个元素或将其删除。而且中途改变比较优先级函数会导致混乱……但这并不能掩盖它的功能的强大性,下面是一版用STL优先队列堆排序写的代码:

#include<iostream>
#include<queue>//优先队列的头文件,它还含有另一种数据结构:队列--queue 
using namespace std;
struct node{
    int time;
    int value; 
}t;
priority_queue<node> a;
bool operator<(const node &x,const node &y){//这里比较函数的名字不能改,两个const和两个&也不能去掉
    return x.time<y.time;
}//定义优先队列的比较函数,这个和sort相反,是使得排序后前后代入可以return false,具体细节请自行了解吧
int c,n;
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        n++;
        t.value=c;
        t.time=n;
        a.push(t);//将元素输入优先队列 
    }
    while(!a.empty()){
        cout<<a.top().value<<" ";//.top()返回优先队列的第一个元素
        a.pop();//删除优先队列的第一个元素 
    }
    return 0;
} 

6.搜索

有一句话叫"骗分过样例,暴力出奇迹",就是靠搜索来骗分。搜索是一种实用性与弊端并存的算法,思考难度不高,运行稳定,唯一的缺陷就是很容易超时。避免的方法一般是剪枝,这里就不深入讲了。下面给大家介绍一下两种最基本的搜索算法:

(1)深度优先搜索(DFS)

DFS是最常用的搜索方式,代码量小,思路简单,优化前后效果明显,就是容易超时……

有人会问这个题和DFS有什么关系吗?有的。我们把输入的数据按输入先后依次连接起来,之后从第一个元素开始DFS递归到底,然后按照出栈序输出,不就把这个题做出来了吗?

大家听不懂也不要紧,我们下面慢慢讲一下DFS:

DFS全称叫深度优先搜索,它的核心思想就是通过函数递归一直搜索到搜索树的叶子节点,然后递归回溯到之前的节点,再搜索其他未搜索过的搜索子树(我知道我讲不懂,就权当抛砖引玉了,有兴趣的同学可以自行了解一下)。

代码如下:

#include<iostream>
#include<vector>
using namespace std;
struct node{
    int value;
    vector<int> to;//存储这个元素指向的所有元素 
}a[101];
int c,n=0;
void dfs(int x){
    for(vector<int>::iterator it=a[x].to.begin();it<a[x].to.end();it++){
        dfs(*it);
    }//遍历这个元素的所有搜索子树并分别递归
    cout<<a[x].value<<" ";
    return;
} 
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        n++;
        a[n].value=c;
        a[n-1].to.push_back(n);
    }
    dfs(1);//从第一个元素开始递归DFS搜索 
    return 0;
} 

(2)广度优先搜索(BFS)

如果说深度优先搜索是用时间换空间,那么广度优先搜索就是用空间换时间。广度优先搜索代码量也不大,但是思路可能会比深度优先搜索难理解一点,而且很难优化,但是大部分时候广度优先搜索的时间复杂度都要大大优于深度优先搜索。

广度优先搜索的核心思路在于每次向下搜索时不是每次都走到搜索树的末端,而是将搜索树的整个下一层存入一个队列,然后按照出队序搜索,如果搜索到了满足条件的结果或队列为空就结束搜索。(我知道我又没讲明白,还是请感兴趣的同学自己去了解一下吧)

顺道科普一下队列:这是一种先入先出,后入后出的数据结构。相对于栈没有那么高的可操作性,但是也很实用。STL中也有一种专门为队列设计的数据结构:queue。

BFS做法和DFS做法的区别在于:DFS是建立在栈的基础上的,而BFS是建立在队列的基础上的。因此在用BFS就不能再从第一个元素开始搜索了,而要从最后一个 元素开始向前搜索。代码如下:

#include<iostream>
#include<vector>
#include<queue>//queue的头文件 
using namespace std;
struct node{
    int value,num;
    vector<node> from;//存储指向这个元素的所有元素 
}a[101];
int c,n=0;
queue<node> q;
void bfs(node x){
    q.push(x);
    while(!q.empty()){//.empty()返回队列是否为空 
        cout<<q.front().value<<" ";//.front()返回队头元素
        for(vector<node>::iterator it=q.front().from.begin();it<q.front().from.end();it++){
            if((*it).num!=0)q.push(*it);//.push()将括号内的元素压入队列 
        }
        q.pop();//.pop()删除队头元素 
    }
    return;
} 
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        n++;
        a[n].value=c;
        a[n].num=n;
        a[n].from.push_back(a[n-1]);
    }
    bfs(a[n]);//从最后一个元素开始BFS搜索 
    return 0;
} 

7.二叉树后序(中序)遍历

为了精简题解长度,我就不再科普树了……(对不起,想了解的同学自行了解吧)其实这个方法纯粹凑数,办法就是将每一个数据都存到它的上一个数据的左儿子里(后序遍历就无所谓了),然后中序或者后序遍历……中序遍历我就不写了,后序遍历代码如下:

#include<iostream>
using namespace std;
struct tree{
    int value;
    int left,right;//左儿子,右儿子 
}a[101];
int c,n=0;
void dfs(int x){
    if(a[x].left!=0){
        dfs(a[x].left);
    }
    if(a[x].right!=0){
        dfs(a[x].right);
    }
    cout<<a[x].value<<" "; 
    return;
} 
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        n++;
        a[n].value=c;
        a[n-1].left=n;
    }
    dfs(1);//以第一个元素为根开始遍历 
    return 0;
} 

8.拓扑排序

(图论基础就不在这里普及了,以下图论算法只讲思路)

要讲拓扑排序,首先就要讲一下拓扑图:拓扑图就是一个有向无环图,这就是拓扑图的主要属性了。拓扑排序则是指找出一个遍历序,使得没有任意两个点存在晚遍历到的点有指向早遍历到的点的边,这个遍历序就叫做拓扑序。对于一个拓扑图,可能不只有一个拓扑序。

求拓扑排序的方法一般是:将所有入度为零的点存入一个队列,然后每次访问队首,将队首指向的元素的入度减1,然后将队首存入另一个队列用作遍历序的存储或直接输出。当一个点被更新入度直到入度为零后,就将这个点存入存入度为零的点的队列。直到这个队列为空,结束算法。

对于这个题,我们可以将各个数据按照输入先后顺序反向建边,然后这个图的拓扑序就是我们需要的答案了。代码如下:

#include<iostream>
#include<vector>
#include<queue>
using namespace std;
struct node{
    int pre;//入度
    int value; 
    vector<int> to;//储存这个元素所指向的所有元素 
}a[101];
queue<node> q;
int c,n=0;
void solve(){
    while(!q.empty()){
        cout<<q.front().value<<" ";
        for(vector<int>::iterator it=q.front().to.begin();it<q.front().to.end();it++){
            a[*it].pre--;
            if(a[*it].pre==0&&*it!=0) q.push(a[*it]);
        }
        q.pop();
    }
    return; 
}
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        n++;
        a[n].value=c;
        a[n].to.push_back(n-1);
        a[n-1].pre++;//被指向的元素入度加1 
    }
    for(int i=1;i<=n;i++){
        if(a[i].pre==0) q.push(a[i]);
    }
    solve();
    return 0;
} 

9.最短路算法

最短路算是NOIP所有考点中难度中上的一个知识点了。最短路问题即求一个点到另一个点的最短路径长度(其他最短路基础知识就先不科普了,有兴趣的同学可以自行了解一下)。本题与最短路的关联是:将所有元素按照输入先后顺序建边,然后求出到最后一个元素的最短路,按距离大小排序后输出,即得所求答案。

(1)dijkstra算法

dijkstra算法是被使用得最广的一种求单源最短路的算法。它最大的优点就是稳定,但同时也有两个缺点:一是不能应对有负权边的图,二是时间复杂度不是很优,是 $O(n^2)$ 的。不过可以借助优先队列优化来将时间复杂度优化为 $O(n \ log \ n)$ 。

dijkstra的核心思想是:如果到某个点的距离已经是目前可从已确定点到达的最短距离了,那么从距离更远的点就不可能以更短的距离到达这个点了。这也是它不能应对有负权边的图的原因(同学们可以思考一下为什么)。

代码如下:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int INF=2147483647;
struct node{
    int value;
    int dis;//储存这个点到源点的最短距离 
    bool visit;//存储这个点的距离是否已经被确定为最短距离 
    vector<int> to;//储存这个元素所连接的所有元素 
}a[101];
int c,n=0;
void dijkstra(int x){
    int q[101],head=0,tail=0;//存储已经确定最短路的点,这里选择手打队列 
    int minn,now; 
    a[x].dis=0;
    a[x].visit=true;
    q[tail++]=x;//不要搞混++i和i++的区别 
    for(int i=1;i<n;i++){
        minn=INF;
        for(int j=head;j<tail;j++)
        for(vector<int>::iterator it=a[q[j]].to.begin();it<a[q[j]].to.end();it++){
            if(a[q[j]].dis+1<minn&&!a[*it].visit){
                minn=a[q[j]].dis+1;
                now=*it;
            } 
        }//循环遍历求当前所能到达的距离最近的点 
        q[tail++]=now;
        a[now].visit=true;
        a[now].dis=minn; 
    }
    return; 
}
int cmp(node x,node y){
    return x.dis<y.dis;
}
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        n++;
        a[n].value=c;
        a[n].to.push_back(n-1);
        a[n-1].to.push_back(n);//因为是无向边所以边连接的两个元素都要建边 
    }
    dijkstra(n);//以n作为源点求最短路 
    sort(a+1,a+n+1,cmp);
    for(int i=1;i<=n;i++){
        cout<<a[i].value<<" ";
    }
    return 0;
} 

(2)spfa算法

spfa算法全称为Shortest Path Faster Algorithm,直译的话可以称为"最短路快速算法"。spfa的优点就是解决了dijkstra最大的缺点:不能应对有负权边的图。但同时它也有着自己的缺点,那就是如果出题人很善良,你的时间复杂度可能只有 $O(n)$ 级别,但是如果出题人故意卡你的数据的话,算法复杂度就可能是 $O(nm)$ 的……

spfa的核心思路是用已访问的点去不断的更新它所连接的所有点的最短距离的值。如果成功更新了,那么说明被更新的点之前更新别的点的结果偏大,于是就应当把被更新点入队,再次去更新其他的点,直到不能更新为止(善于思考的同学可以想一下为什么这个算法容易被卡数据)。下面就是spfa的代码:

#include<iostream>
#include<algorithm>
#include<vector>
#include<queue> 
using namespace std;
const int INF=2147483647;
struct node{
    int value;
    int dis=INF; 
    bool in;//存储这个点是否已经在队列中 
    vector<int> to;//储存这个元素所连接的所有元素 
}a[101];
int c,n=0;
void spfa(int x){
    queue<int> q;;
    int minn,now; 
    a[x].dis=0;
    a[x].in=true;
    q.push(x);
    while(!q.empty()){
        for(vector<int>::iterator it=a[q.front()].to.begin();it<a[q.front()].to.end();it++){
            if(a[q.front()].dis+1<a[*it].dis){
                a[*it].dis=a[q.front()].dis+1;
                if(!a[*it].in){
                    q.push(*it);
                    a[*it].in=true;
                }//如果这个点已经在队列中就不用重复入队了 
            } 
        }
        a[q.front()].in=false;
        q.pop();
    }
    return; 
}
int cmp(node x,node y){
    return x.dis<y.dis;
}
int main(){
    while(1){
        cin>>c;
        if(c==0) break;
        n++;
        a[n].value=c;
        a[n].to.push_back(n-1);
        a[n-1].to.push_back(n);//因为是无向边所以边连接的两个元素都要建边 
    }
    spfa(n);//以n作为源点求最短路 
    sort(a+1,a+n+1,cmp);
    for(int i=1;i<=n;i++){
        cout<<a[i].value<<" ";
    }
    return 0;
} 

看了这么多做法,相信大家都能体会到oi是一门非常神奇的学科。希望大家看完后能够多多思考,把学到的东西转化为自己的知识,化为自己的力量。

好的,本期的 滑稽题解 到此就结束了,让我们下期再见吧!




本题解为作者的处女作,如有不足敬请谅解