算法竞赛刷题Day01:链表

164 阅读7分钟

洛谷P1996:约瑟夫问题

题目描述

nn 个人围成一圈,从第一个人开始报数,数到 mm 的人出列,再由下一个人重新从 11 开始报数,数到 mm 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。

注意:本题和《深入浅出-基础篇》上例题的表述稍有不同。书上表述是给出淘汰 n1n-1 名小朋友,而该题是全部出圈。

  • 这题的解法都是使用链表模拟法,时间复杂度高达O(mn)O(mn),空间复杂度O(n)O(n)
  • 数学优化解法只能求最后一个人,时间复杂度O(n)O(n),空间复杂度O(1)O(1)

输入格式

输入两个整数 n,mn,m

输出格式

输出一行 nn 个整数,按顺序输出每个出圈人的编号。

样例 #1

样例输入 #1

10 3

样例输出 #1

3 6 9 2 7 1 8 5 10 4

提示

1m,n1001 \le m, n \le 100

动态单循环链表解法

#include<bits/stdc++.h>
using namespace std;
struct Node {
    int id;
    Node* next=nullptr;
    Node(int _id): id(_id) {}
};
int main() {
    Node* head=new Node(1);
    Node* cur=head, *prev=nullptr;
    int n, m;
    cin>>n>>m;
    for(int i=2; i<=n; i++) {
        Node* node=new Node(i);
        cur->next=node;
        cur=node;
    }
    cur->next=head;
    // 以上是动态单循环链表的建立过程
    cur=head; 		// 回到初始状态,开始游戏
    while(--n) { 	// 为的是最后一位单独输出
        for(int i=1; i<m; i++) { 	// 注意这里只进行m-1次
            prev=cur;
            cur=cur->next;
        }
        cout<<cur->id<<' '; 		// 数到m的人,带空格
        prev->next=cur->next; 		// 为的是删除数到m的节点
        delete cur;
        cur=prev->next; 			// 当前从数到m的下一个人开始
    }
    cout<<cur->id; 	// 输出最后一个人,不带空格
    delete cur;
}

静态单循环链表解法

#include<bits/stdc++.h>
using namespace std;
#define N 105
struct Node {
    int id, next;
} node[N];
int main() {
    int n, m, cur, prev;
    cin>>n>>m;
    for(int i=1; i<=n; i++) {
        node[i].id=i;
        node[i].next=i+1;
    }
    node[n].next=1;
    // 以上是静态单循环链表的建立过程
    cur=1;
    while(--n) {
        for(int i=1; i<m; i++) {
            prev=cur;
            cur=node[cur].next;
        }
        cout<<cur<<' ';
        node[prev].next=node[cur].next;
        cur=node[prev].next;
    }
    cout<<cur;
}

一维数组实现静态单循环链表

  • 本题静态双循环链表略
  • 注意到node[i]的id就是它本身,所以可以用它的值来表示next
#include<bits/stdc++.h>
using namespace std;
#define N 105
int node[N];
int main() {
    int n, m, cur, prev;
    cin>>n>>m;
    for(int i=1; i<n; i++)  node[i]=i+1;
    node[n]=1;
    // 以上是静态单循环链表的建立过程
    cur=1;
    while(--n) {
        for(int i=1; i<m; i++) {
            prev=cur;
            cur=node[cur];
        }
        cout<<cur<<' ';
        node[prev]=node[cur];
        cur=node[prev];
    }
    cout<<cur;
}

STL list实现

#include<bits/stdc++.h>
using namespace std;
int main() {
    int n, m;
    cin>>n>>m;
    list<int> node;
    for(int i=1; i<=n; i++) node.push_back(i);
    list<int>::iterator it=node.begin(); // 以后可以用auto
    while(node.size()>1) {
        for(int i=1; i<m; i++) {
            it++;
            if(it==node.end()) it=node.begin(); // 循环链表
        }
        cout<<*it<<' ';
        auto next=++it; // 这里加上
        if(next==node.end()) next=node.begin(); // 循环链表
        node.erase(--it); // 这里减回来,node.size()自动减1
        it=next;
    }
    cout<<*it;
}

队列解法(更符合直觉)

#include<bits/stdc++.h>
using namespace std;
int main() {
    int n, m, cur=1;
    cin>>n>>m;
    queue<int> q;
    for(int i=1; i<=n; i++) q.push(i);
    while(q.size()>1) {
        if(cur==m) {
            cout<<q.front()<<' ';
            q.pop();
            cur=1; // 重新开始
        } else {
            q.push(q.front());
            q.pop();
            cur++;
        }
    }
    cout<<q.front();
}

倒推法(只能求剩下的人)

  • 洛谷P8671
  • 注意:这种方法并不适用于求所有人的出队顺序,前者只能老老实实模拟
  • 假设编号从0~n-1,剩下的最后一个数字pos=0
  • 那么它在上一轮也是安全的,下标pos=(pos+m)%2,原因是被移走的m-1的下一位最安全
  • 下一轮,下标pos=(pos+m)%3,以此类推,到第n轮,下标pos+1即为所求(编号从1开始)
#include<bits/stdc++.h>
using namespace std;
int Joseph(int n, int m){
    int pos=0;
    for(int i=2; i<=n; i++) pos=(pos+m)%i;
    return pos+1;
}
int main() {
    int n, m;
    cin>>n>>m;
    cout<<Joseph(n, m);
}

洛谷P1160:队列安排

题目描述

一个学校里老师要将班上 NN 个同学排成一列,同学被编号为 1N1\sim N,他采取如下的方法:

  1. 先将 11 号同学安排进队列,这时队列中只有他一个人;

  2. 2N2\sim N 号同学依次入列,编号为 ii 的同学入列方式为:老师指定编号为 ii 的同学站在编号为 1(i1)1\sim(i-1) 中某位同学(即之前已经入列的同学)的左边或右边;

  3. 从队列中去掉 MM 个同学,其他同学位置顺序不变。

在所有同学按照上述方法队列排列完毕后,老师想知道从左到右所有同学的编号。

输入格式

第一行一个整数 NN,表示了有 NN 个同学。

2N2\sim N 行,第 ii 行包含两个整数 k,pk,p,其中 kk 为小于 ii 的正整数,pp00 或者 11。若 pp00,则表示将 ii 号同学插入到 kk 号同学的左边,pp11 则表示插入到右边。

N+1N+1 行为一个整数 MM,表示去掉的同学数目。

接下来 MM 行,每行一个正整数 xx,表示将 xx 号同学从队列中移去,如果 xx 号同学已经不在队列中则忽略这一条指令。

输出格式

一行,包含最多 NN 个空格隔开的整数,表示了队列从左到右所有同学的编号。

样例 #1

样例输入 #1

4
1 0
2 1
1 0
2
3
3

样例输出 #1

2 4 1

提示

【样例解释】

将同学 22 插入至同学 11 左边,此时队列为:

2 1

将同学 33 插入至同学 22 右边,此时队列为:

2 3 1

将同学 44 插入至同学 11 左边,此时队列为:

2 3 4 1

将同学 33 从队列中移出,此时队列为:

2 4 1

同学 33 已经不在队列中,忽略最后一条指令

最终队列:

2 4 1

【数据范围】

对于 20%20\% 的数据,1N101\leq N\leq 10

对于 40%40\% 的数据,1N10001\leq N\leq 1000

对于 100%100\% 的数据,1<MN1051<M\leq N\leq 10^5

动态单链表超时代码⚠

  • 链表需要频繁查找第k个同学的位置,效率低下
#include<bits/stdc++.h>
using namespace std;
int main() {
    int n, m;
    cin>>n;
    list<int> node;
    node.push_back(1);
    for(int i=2; i<=n; i++){
        int k, p; cin>>k>>p;
        auto it=node.begin();
        for(; *it!=k; it++);
        if(p==0) node.insert(it, i);
        else node.insert(++it, i);
    }
    cin>>m;
    for(int i=0; i<m; i++){
        int x; cin>>x;
        auto it=node.begin();
        for(; *it!=x; it++);
        if(it!=node.end()) node.erase(it);
    }
    for(auto id: node) cout<<id<<' ';
}

静态双链表优化版

  • 关键在于提高链表读取的效率,空间换时间
#include<bits/stdc++.h>
using namespace std;
#define N 100005
struct Node {
    int left=-1, right=-1;
} node[N];
int main() {
    int n, m, cur;
    cin>>n;
    node[0].right=1; 	// 把0号作为虚拟头结点
    node[1].left=0;
    node[1].right=n+1; 	// 右边界为n+1
    for(int i=2; i<=n; i++) {
        int k, p;
        cin>>k>>p;
        if(p==0) {
            // 先解决插入节点的左右邻居
            node[i].left=node[k].left;
            node[i].right=k;
            // 再解决左右邻居的左右孩子
            node[node[i].right].left=i;
            node[node[i].left].right=i;
        } else {
            // 先解决插入节点的左右邻居
            node[i].left=k;
            node[i].right=node[k].right;
            // 再解决左右邻居的左右孩子
            node[node[i].right].left=i;
            node[node[i].left].right=i;
        }
    }
    cin>>m;
    for(int i=0; i<m; i++) {
        int x;
        cin>>x;
        if(node[x].left==-1 && node[x].right==-1) continue;
        // 记录下左右邻居,删除节点
        int left=node[x].left, right=node[x].right;
        node[left].right=right;
        node[right].left=left;
        node[x].left=node[x].right=-1;
    }
    cur=0;
    while(node[cur].right!=n+1) {
        cur=node[cur].right;
        cout<<cur<<' ';
    }
}

vector版(更符合代码随想录风格)

#include<bits/stdc++.h>
using namespace std;
struct Node {
    int left=-1, right=-1;
};
int main() {
    int n, m;
    cin>>n;
    vector<Node> v(n+2);
    // 初值能保证不越界
    v[0].right=1; // 虚拟头结点
    v[1].left=0;
    v[1].right=n+1;
    for(int i=2; i<=n; i++) {
        int k, p;
        cin>>k>>p;
        if(p==0) {
            v[i].left=v[k].left;
            v[i].right=k;
            v[v[i].left].right=i;
            v[v[i].right].left=i;
        } else {
            v[i].left=k;
            v[i].right=v[k].right;
            v[v[i].left].right=i;
            v[v[i].right].left=i;
        }
    }
    cin>>m;
    for(int i=0; i<m; i++) {
        int x;
        cin>>x;
        if(v[x].left==-1 && v[x].right==-1) continue;
        int left=v[x].left, right=v[x].right;
        v[left].right=right;
        v[right].left=left;
        v[x].left=v[x].right=-1;
    }
    int cur=0;
    while(v[cur].right!=n+1) {
        cur=v[cur].right;
        cout<<cur<<' ';
    }
}

参考资料

[1] 洛谷题解

[2] 算法竞赛(上册)(罗勇军)