[关闭]
@RabbitHu 2017-08-21T17:48:05.000000Z 字数 1442 阅读 1868

线段树 x 约瑟夫 —— CodeVS 1282

题解


原题在这里!

题目描述 Description

有编号从1到N的N个小朋友在玩一种出圈的游戏。开始时N个小朋友围成一圈,编号为I+1的小朋友站在编号为I小朋友左边。编号为1的小朋友站在编号为N的小朋友左边。首先编号为1的小朋友开始报数,接着站在左边的小朋友顺序报数,直到数到某个数字M时就出圈。直到只剩下1个小朋友,则游戏完毕。
现在给定N,M,求N个小朋友的出圈顺序。

输入描述 Input Description

唯一的一行包含两个整数N,M。(1<=N,M<=30000)

输出描述 Output Description

唯一的一行包含N个整数,每两个整数中间用空格隔开,第I个整数表示第I个出圈的小朋友的编号。

样例输入 Sample Input

5 3

样例输出 Sample Output

3 1 5 2 4

思路

一般的约瑟夫问题解法(暴力模拟)的复杂度是 O(mn),非常慢,像这道题一定会爆掉的!

但是有了线段树以后我们可以得到一个非常 妙~~~ 的解法,它的复杂度是 O(nlogn)。

首先我们考虑,现在有一排圆滚滚的熊孩子

  1. OOOOOOOOO

把他们从0开始编号:

  1. OOOOOOOOO
  2. 012345678

假设我们让数到3的熊孩子退出,那么第一个退出的是当前队伍里的第三个——也就是2号熊孩子,队伍变成这样:

  1. OOOOOOOO
  2. 01345678

第二个退出的熊孩子是当前队伍里往下数三个——也就是5号熊孩子。

以此类推(当然,数到队伍末尾的时候要模当前队伍的长度),显然我们每次都知道当前队伍中第几个会退出。

那么问题来了:我们需要知道当前队伍中的第p个人在初始队伍中的编号,并输出。

这时候我们就可以用线段树了。

这棵线段树非常简单:一个节点存储的是它所代表的区间[i,j]中,还没退出的人数。这里的i,j都是初始队伍中的编号。

那么:

  1. [N]
  2. |\
  3. [2*N][2*N+1]

我们要找N代表的区间中第p个还在队伍中的熊孩子(从0开始数),要判断一下:
1.如果 p < data[2*N], 就在左儿子里找第p个;
2.如果 p >= data[2*N], 就在右儿子里找第 p-data[2*N] 个。

这样就可以找到啦。

AC代码

  1. #include <cstdio>
  2. using namespace std;
  3. int data[120005], n, m;
  4. void build(int i, int l, int r){
  5. if(l == r){
  6. data[i] = 1;
  7. return;
  8. }
  9. int mid = (l + r) >> 1;
  10. build(i<<1, l, mid);
  11. build(i<<1|1, mid+1, r);
  12. data[i] = data[i<<1] + data[i<<1|1];
  13. }
  14. void dlt(int i, int p, int l, int r){
  15. if(l == r){
  16. printf("%d ", l+1);
  17. data[i] = 0;
  18. return;
  19. }
  20. int mid = (l + r) >> 1;
  21. if(data[i << 1] > p) dlt(i << 1, p, l, mid);
  22. else dlt(i << 1 | 1, p - data[i << 1], mid + 1, r);
  23. data[i] = data[i<<1] + data[i<<1|1];
  24. }
  25. int main() {
  26. scanf("%d%d", &n, &m);
  27. int p = 0;
  28. build(1, 0, n-1);
  29. for(int q = 1; q <= n; q++){
  30. p = (p + m - 1) % (n - q + 1);
  31. dlt(1, p, 0, n - 1);
  32. }
  33. return 0;
  34. }

作者:胡小兔
批评指正敬请联系:yuanxiaodidl@163.com
打赏一根胡萝卜(真的吗?):

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注