洛谷 P1996 约瑟夫问题之题解

题目链接

https://www.luogu.com.cn/problem/P1996

洛谷P1996 约瑟夫问题 题面

题目描述

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

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

输入格式

输入两个整数 n , m n,m n,m

输出格式

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

样例1

输入
10 3
输出 #1
3 6 9 2 7 1 8 5 10 4

说明 / 提示

1 ≤ m , n ≤ 100 1 \le m, n \le 100 1m,n100

注意:本题多解!

我们讲三个解法:

  1. STL list
  2. 动态链表
  3. 静态链表

解法 1:STL list

在算法竞赛或工程项目中常常使用 C++ 中的 STL list。list 是双向链表,由标准模板库(Standard Template Library, STL)管理,通过指针访问节点数据,高效率地插入和删除。

以下代码用 STL list 实现了这道题。

// 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();
	while (node.size() > 1) {                           // list 的大小由 STL 自己管理
		for (int i = 1; i < m; i++) {                   // 数到 m
			it++;
			if (it == node.end()) it = node.begin();    // 循环:到末尾后再回头
		}
		cout << *it << " ";
		list<int>::iterator next = ++it;
		if (next == node.end()) next = node.begin();   // 循环链表
		node.erase(--it);                               // 删除节点,node.size() 自动减 1
		it = next;
	}
	cout << *it;
	return 0;
}

解法 2:动态链表

动态链表需要临时分配链表节点,使用完毕后释放链表节点。动态链表的优点是能及时释放空间,不是用多余内存;缺点是需要管理空间,容易出错。动态链表是“教科书式”的标准做法。

以下代码用动态单向链表实现了这道题。

// 动态链表
#include <bits/stdc++.h>
using namespace std;
struct node {
    int data;
    node *next;
};
int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    node *head, *p, *now, *prev;
    head = new node;
    head -> data = 1;
    head -> next = NULL;
    now = head;
    for (int i = 2; i <= n; i++) {
        p = new node;
        p -> data = i;
        p -> next = NULL;
        now -> next = p;
        now = p;
    }
    now -> next = head;
    prev = now, now = head;
    while ((n--) > 1) {
        for (int i = 1; i < m; i++) {
            prev = now;
            now = now -> next;
        }
        printf("%d ", now -> data);
        prev -> next = now -> next;
        delete now;
        now = prev -> next;
    }
    printf("%d", now -> data);
    delete now;
    return 0;
}

解法 3:静态链表

提示:动态链表虽然“标准”,但是竞赛中一般不用。算法竞赛队内存管理要求不严格,为加快编码速度,一般就在题目允许的存储空间内静态分配,省去了动态分配和释放的麻烦。

静态链表使用预先分配的一段连续空间存储链表。从物理存储的意义上讲,“连续空间”并不符合链表的本义,因为链表的优点就是能克服连续存储的弊端。但是,用连续空间实现的链表,在逻辑上是成立的。具体有两种做法:①定义链表结构体数组,和动态链表的结构差不多;②使用一维数组,直接在数组上进行链表操作。

在此,我们会给出 3 3 3 段代码:用结构体数组实现单向静态链表、用结构体数组实现双向静态链表,用一维数组实现单向静态链表。

1. 用结构体数组实现静态链表

用结构体数组实现单向静态链表,注意静态分配应该定义在全局1,不能定义在函数内部。

// 结构体数组实现单向静态链表
#include <bits/stdc++.h>
const int N = 105;								//定义静态链表的空间大小
struct node {									//单向链表
	int id, nextid;								//单向指针
//  int data;	                                //如有必要,定义一个有意义的数据
} nodes[N];                                     	//定义在全局的静态分配
int main () {
	int n, m;
	scanf("%d%d", &n, &m);	
    nodes[0].nextid = 1;
	for(int i = 1; i <= n; i++) {
		nodes[i].id = i;
		nodes[i].nextid = i + 1;
	}
	nodes[n].nextid = 1;	                    //循环链表:尾指向头
	int now = 1, prev = 1;  	                //从第 1 个节点开始
	while ((n--) > 1) {
		for(int i = 1; i < m; i++) {
			prev = now;
			now = nodes[now].nextid;
		}                                       //数到 m 停下
		printf("%d ", nodes[now].id);       	//带空格打印	
		nodes[prev].nextid = nodes[now].nextid; //跳过 now 节点,即删除 now
		now = nodes[prev].nextid;	            //新的 now
	}
	printf("%d", nodes[now].nextid);	        //打印最后一个节点,后面不带空格	
	return 0;
}

2. 用结构体数组实现双向静态链表

// 结构体数组实现双向静态链表
#include <bits/stdc++.h>
const int N = 105;
struct node {                                                //双向链表
    int id;                                                  //节点编号
  //int data;                                                //如有必要,定义一个有意义的数据
    int preid, nextid;                                       //前一个节点,后一个节点
} nodes[N];
int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    nodes[0].nextid = 1;
    for (int i = 1; i <= n; i++) {                           //建立链表
        nodes[i].id = i;
        nodes[i].preid = i - 1;                              //前节点
        nodes[i].nextid = i + 1;                             //后节点
    }
    nodes[n].nextid = 1;                                     //循环链表:尾指向头
    nodes[1].preid = n;                                      //循环链表:头指向尾
    int now = 1;                                             //从第一个节点开始
    while ((n--) > 1) {
        for (int i = 1; i < m; i++) now = nodes[now].nextid; //数到 m 停下
        printf("%d ", nodes[now].id);                        //打印,后面带空格
        int prev = nodes[now].preid, next = nodes[now].nextid;
        nodes[prev].nextid = nodes[now].nextid;              //删除 now
        nodes[next].preid = nodes[now].preid;
        now = next;                                          //新的开始
    }
    printf("%d", nodes[now].nextid);                         //打印最后一个节点,后面不带空格
    return 0;
}

3. 用一维数组实现单向静态链表

这是最简单的实现方法。定义一个一位数组 nodes[   ] \text{nodes[\,]} nodes[] nodes[   i   ] \text{nodes[}\,i\,\text{]} nodes[i] i i i 就是节点的值,而 nodes[   i   ] \text{nodes[}\,i\,\text{]} nodes[i] 的值是下一个节点,它的使用环境很有限,因为他的节点只能存一个数据,就是 i i i

// 结构体数组实现双向静态链表
#include <bits/stdc++.h>
int nodes[150];
int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n-1; i++) nodes[i] = i + 1; //nodes[i] 的值就是下一个节点
    nodes[n] = 1;                                    //循环链表:尾指向头
    int now = 1, prev = 1;                           //从第一个节点开始
    while ((n--) > 1) {
        for (int i = 1; i < m; i++) {
            prev = now; now = nodes[now];            //数到 m 停下
        }
        printf("%d ", now);                          //打印,后面带空格
        nodes[prev] = nodes[now];                    //跳过 now 节点,即删除 now
        now = nodes[prev];                           //新的开始
    }
    printf("%d", now);                               //打印最后一个节点,后面不带空格
    return 0;
}

附:一组洛谷的评测数据

输入

100 3

输出

3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 99 2 7 11 16 20 25 29 34 38 43 47 52 56 61 65 70 74 79 83 88 92 97 1 8 14 22 28 35 41 49 55 62 68 76 82 89 95 4 13 23 32 44 53 64 73 85 94 5 19 37 50 67 80 98 17 40 59 86 10 46 77 26 71 31 100 58 91

P.S. 这组样例只是帮大家查错,尤其是灰名的,请勿用来打表!


  1. 大数组定义在全局的原因是有的评测环境的栈空间很小,大数组定义在局部占用了栈空间导致爆栈。现在各大在线判题系统(Online Judge, OJ)和比赛应该都会设置编译命令使栈空间等于内存大小,不会出现爆栈。虽然如此,在一些编译器中,大静态数组定义在函数内部会出错。 ↩︎

P3922 题目《改变队列》(暂译名,具体题目请以平台为准)是一道涉及队列操作与数据结构设计的题目。题目的核心要求是通过一系列操作来实现队列中元素的调整,以满足特定条件。 ### 问题分析 题目通常要求维护一个队列,支持以下操作: 1. **入队**:将一个元素加入队列末尾。 2. **出队**:移除队列头部的元素。 3. **查询队列中某个特定位置的元素**。 4. **对队列中的元素进行某种形式的修改**(如更新某个位置的值)。 这类问题通常需要一个既能高效进行队列操作又能支持随机访问的数据结构。标准库中的 `std::queue` 并不能很好地支持随机访问,因此可以考虑使用 `std::deque` 或者 `std::vector` 来实现更灵活的操作。 ### 解题思路 - **数据结构选择**:使用 `std::deque` 或 `std::vector` 来模拟队列,因为它们支持高效的头部和尾部操作,同时也能通过索引进行随机访问。 - **操作实现**: - 入队操作直接使用 `push_back()`。 - 出队操作可以通过维护一个偏移量或使用 `pop_front()` 来实现。 - 查询特定位置的元素可以通过索引直接访问。 - 修改操作也可以通过索引直接完成。 ### 示例代码 以下是一个基于 `std::vector` 的实现示例: ```cpp #include <iostream> #include <vector> using namespace std; int main() { int q; cin >> q; vector<int> queue; int front = 0; // 模拟队列的头部位置 for (int i = 0; i < q; ++i) { string op; cin >> op; if (op == "push") { int val; cin >> val; queue.push_back(val); } else if (op == "pop") { front++; // 模拟出队 } else if (op == "query") { int pos; cin >> pos; cout << queue[front + pos - 1] << endl; } else if (op == "modify") { int pos, val; cin >> pos >> val; queue[front + pos - 1] = val; } } return 0; } ``` ### 复杂度分析 - **入队和出队**:时间复杂度为 $O(1)$。 - **查询和修改**:由于使用了随机访问,时间复杂度也为 $O(1)$。 ### 优化建议 - 如果数据量较大,可以考虑使用 `std::deque`,它在头部和尾部的插入和删除操作效率更高。 - 对于频繁的中间位置访问和修改操作,`std::vector` 仍然是一个不错的选择,因为它的内存是连续的,访问速度较快。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值