洛谷P1996:约瑟夫问题
题目描述
个人围成一圈,从第一个人开始报数,数到 的人出列,再由下一个人重新从 开始报数,数到 的人再出圈,依次类推,直到所有的人都出圈,请输出依次出圈人的编号。
注意:本题和《深入浅出-基础篇》上例题的表述稍有不同。书上表述是给出淘汰 名小朋友,而该题是全部出圈。
- 这题的解法都是使用链表模拟法,时间复杂度高达,空间复杂度
- 数学优化解法只能求最后一个人,时间复杂度,空间复杂度
输入格式
输入两个整数 。
输出格式
输出一行 个整数,按顺序输出每个出圈人的编号。
样例 #1
样例输入 #1
10 3
样例输出 #1
3 6 9 2 7 1 8 5 10 4
提示
动态单循环链表解法
#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:队列安排
题目描述
一个学校里老师要将班上 个同学排成一列,同学被编号为 ,他采取如下的方法:
-
先将 号同学安排进队列,这时队列中只有他一个人;
-
号同学依次入列,编号为 的同学入列方式为:老师指定编号为 的同学站在编号为 中某位同学(即之前已经入列的同学)的左边或右边;
-
从队列中去掉 个同学,其他同学位置顺序不变。
在所有同学按照上述方法队列排列完毕后,老师想知道从左到右所有同学的编号。
输入格式
第一行一个整数 ,表示了有 个同学。
第 行,第 行包含两个整数 ,其中 为小于 的正整数, 为 或者 。若 为 ,则表示将 号同学插入到 号同学的左边, 为 则表示插入到右边。
第 行为一个整数 ,表示去掉的同学数目。
接下来 行,每行一个正整数 ,表示将 号同学从队列中移去,如果 号同学已经不在队列中则忽略这一条指令。
输出格式
一行,包含最多 个空格隔开的整数,表示了队列从左到右所有同学的编号。
样例 #1
样例输入 #1
4
1 0
2 1
1 0
2
3
3
样例输出 #1
2 4 1
提示
【样例解释】
将同学 插入至同学 左边,此时队列为:
2 1
将同学 插入至同学 右边,此时队列为:
2 3 1
将同学 插入至同学 左边,此时队列为:
2 3 4 1
将同学 从队列中移出,此时队列为:
2 4 1
同学 已经不在队列中,忽略最后一条指令
最终队列:
2 4 1
【数据范围】
对于 的数据,。
对于 的数据,。
对于 的数据,。
动态单链表超时代码⚠
- 链表需要频繁查找第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] 算法竞赛(上册)(罗勇军)