洛谷P1088.火星人【模拟/搜索/康托展开】

  • 题干
    • 题目描述
    • 输入格式
    • 输出格式
    • 输入输出样例
    • 说明/提示
  • 题意
  • 思路一——模拟
    • 分析
    • 上代码
  • 思路二——搜索
    • 分析
    • 上代码
  • 思路三——变进制数与康托展开
    • 理解
      • “次序数列”的引入
      • 全排列与次序数列的一一对应
      • 次序数列、变进制数、康托展开
    • 分析
    • 上代码

题干

题目描述

人类终于登上了火星的土地并且见到了神秘的火星人。人类和火星人都无法理解对方的语言,但是我们的科学家发明了一种用数字交流的方法。这种交流方法是这样的,首先,火星人把一个非常大的数字告诉人类科学家,科学家破解这个数字的含义后,再把一个很小的数字加到这个大数上面,把结果告诉火星人,作为人类的回答。

火星人用一种非常简单的方式来表示数字――掰手指。火星人只有一只手,但这只手上有成千上万的手指,这些手指排成一列,分别编号为1,2,3…1,2,3…。火星人的任意两根手指都能随意交换位置,他们就是通过这方法计数的。

一个火星人用一个人类的手演示了如何用手指计数。如果把五根手指――拇指、食指、中指、无名指和小指分别编号为1,2,3,4和5,当它们按正常顺序排列时,形成了55位数12345,当你交换无名指和小指的位置时,会形成55位数12354,当你把五个手指的顺序完全颠倒时,会形成54321,在所有能够形成的120个5位数中,12345最小,它表示1;12354第二小,它表示2;54321最大,它表示120。下表展示了只有3根手指时能够形成的6个3位数和它们代表的数字:

三进制数

123
132
213
231
312
321

代表的数字

1
2
3
4
5
6

现在你有幸成为了第一个和火星人交流的地球人。一个火星人会让你看他的手指,科学家会告诉你要加上去的很小的数。你的任务是,把火星人用手指表示的数与科学家告诉你的数相加,并根据相加的结果改变火星人手指的排列顺序。输入数据保证这个结果不会超出火星人手指能表示的范围。

输入格式

共三行。
第一行一个正整数N,表示火星人手指的数目(1≤N≤10000)。
第二行是一个正整数M,表示要加上去的小整数(1≤M≤100)。
下一行是1到N这N个整数的一个排列,用空格隔开,表示火星人手指的排列顺序。

输出格式

N个整数,表示改变后的火星人手指的排列顺序。每两个相邻的数中间用一个空格分开,不能有多余的空格。

输入输出样例

输入
5
3
1 2 3 4 5
输出
1 2 4 5 3

说明/提示

对于30%的数据,N≤15;

对于60%的数据,N≤50;

对于全部的数据,N≤10000;

题意

题干啰里啰唆半天,翻译成大白话就是——
对于1到N这N个数字构成的N!种全排列,按照它们代表的自然数的大小顺序,建立这N!种排列与自然数1到N!的一个一一映射,并能够双向索引。
当然,在具体的完成题目的过程中,不一定要使程序能做到上述全部,这在之后的题解中可以看出来。
注:下面大部分思路均来源于洛谷题解,用个人理解后的语言重新解释了一下,让自己能够更加理清思路。

思路一——模拟

分析

看到这个题,第一想法是找规律,用人脑去模拟这个找到紧挨着的下一种全排列的过程。
首先我们规定,最高位为数位1,最低位为数位N,方便之后讨论。
有这样几点显然的想法:

  1. 应该尽量改变更大的若干个数位的排列方式,而不是更小的;
  2. 当后x位形成一个递降的序列时,我们必须再往前考虑一位了;
  3. 当a[x]<a[x+1]时,我们不可能改变a[x-1]来获得下一个紧挨着的全排列;

(比如,对于12354,后两位是一个递降的序列,我们不能够只在改变后两位的过程中寻找下一种排列了,这时必须改变后三位;再对于12543,后三位是递降序列,下一个排列必然要改动2了)
有了一些想法之后,我们从头开始模拟,以12345为例:

  1. 第一次操作
    考虑第四位(倒数第二位)是否可以增加?
    发现第四位小于第五位,说明可以操作第四位;在当前排列从后往前找到最小的可增加的数字5;交换4和5,使原排列增大,得到12354;最后使操作位第四位之后的所有位的数字从小到大排列,得到12354;
  2. 第二次操作
    考虑第四位(倒数第二位)是否可以增加?
    发现第四位大于第五位,说明不能操作第四位;
    于是考虑第三位是否可以增加?
    发现第三位小于第四位,说明可以操作第三位;找到最小的可增加的数字4,交换3和4,得到12453;最后使操作位第三位之后的数字从小到大排列,得到12435;
  3. 第三次操作
    考虑第四位是否可以增加?
    发现第四位小于第五位,说明可以操作第四位;从后往前找到最小的可增加的数字5;交换3和5,得到12453;最后使第四位之后的所有数字从小到大,还是得到12453;
  4. 第四次操作
    第四位不能操作,第三位小于第四位,可以操作第三位;从后往前找到最小的数字5,交换4和5,得到12543;最后还是12543;
  5. 第五次操作
    操作第二位;找到3,交换2和3,得到13542;最后让第二位之后的所有数字从小到大,得到13245;

之后全部操作类似,第一次举例,写了很多,方便理解整个过程。从这个过程中不难看出:
在操作第i位的时候,当前的排列中,从第(i+1)位到第N位一定是一个递降的序列,并且第(i+1)位大于第i位
在上述递降的序列中从后往前找到第一个比第i位大的数并交换二者;(因为是从后往前,所以能保证是最小的满足条件的数)
交换之后,从第(i+1)位到第N位仍是一个递降的序列;这时让第(i+1)位到第N位逆序过来,变成一个递增的序列;
然后就得到了下一个紧邻的排列。

上代码

#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 10000;
int b[maxn + 10];
int n, m, i, j;
void init()
{cin >> n >> m;for (int i = 1; i <= n; i++)cin >> b[i];
}void out()
{for (int i = 1; i < n; i++)cout << b[i] << ' ';cout << b[n] << endl;
}void solve()
{while (m--)//一共操作m次{for (i = n - 1; i >= 1; i--)//确定当前应该操作哪一位if (b[i] < b[i + 1])//发现第i位满足要求break;for (j = n; j > i; j--)//确定哪个数和第i位的数交换if (b[j] > b[i])//发现第j位满足要求break;swap(b[i], b[j]);//让第(i+1)位到第N位逆序for (int k = i + 1; k <= (i + 1 + n) / 2; k++)swap(b[k], b[n + i + 1 - k]);}
}int main()
{init();solve();out();return 0;
}

思路二——搜索

分析

首先我们会想到这样一道关于”全排列“的基础题:

这就是一个典型的深度优先搜索模板。先看看这个题的AC代码:

#include<iostream>
#include<iomanip>
using namespace std;
int a[10];
bool used[10];
int n;
void print()
{for (int i = 1; i <= n; i++)cout << setw(5) << a[i];
}
void dfs(int k)
{if (k > n){print();cout << endl;return;}for (int i = 1; i <= n; i++){if (!used[i]){a[k] = i;used[i] = 1;dfs(k + 1);used[i] = 0;}}
}
int main()
{cin >> n;dfs(1);return 0;
}

这样可以得到全部的排列,并且是按顺序的!!!
那么这个题对于我们这个火星人的题目有什么启发吗?
dfs的过程就是,先走出12345,然后一步步回溯,依次得出排列;所以我们从外星人给出的排列设定初始数组,第一次搜索到达的即为外星人给出的排列,再回溯m步,得到的就是我们的目标排列,输出这个答案即可

上代码

#include<iostream>
#include<iomanip>
using namespace std;
const int maxn = 10000;
int a[maxn+10];
bool used[maxn+10];
int n, m, cnt = 0, flag = 0;
void init()
{cin >> n >> m;for (int i = 1; i <= n; i++)cin >> a[i];
}
void out()
{for (int i = 1; i < n; i++)cout << a[i] << ' ';cout << a[n] << endl;
}
void dfs(int k)
{if (flag)return;if (k > n){cnt++;//第一次到达时,仍为初始排列,cnt变为1,所以最后到达时应该等于m+1if (cnt == m + 1)//到达了我们的目标全排列{out();//按要求输出flag = 1;//标记好,一路return直到退出}}for (int i = 1; i <= n; i++){if (!cnt)i = a[k];//保证第一次到达时的数列就是外星人给出的原始数列if (!used[i]){a[k] = i;used[i] = 1;dfs(k + 1);used[i] = 0;}}
}
int main()
{init();dfs(1);return 0;
}

思路三——变进制数与康托展开

理解

参考这两个知乎链接:

迟到的【洛谷日报#187】浅谈康托展开
康托展开怎么理解?回答者:JohnstonXi

给出一个全排列,求它是第几个全排列,叫做康托展开。
给出全排列长度和它是第几个全排列,求这个全排列,叫做逆康托展开。

先不管这些术语。

“次序数列”的引入

我们要引入一个次序数列;
灵感来源是啥?
对于一个全排列,第i位有(n-i+1)种选择;
我们希望实现:当第i位从小到大试完了这(n-i+1)种选择之后,并且后面的每一位也都到达了最大的临界状态,再加1时,就应该进位,同时后面的每一位都退回到最小的状态,让第i-1位开始从小到大尝试(n-i+2)种选择。
这也照应了思路一的朴素模拟过程。

但是对于原始的排列来说,我们并不能确定第i位的(n-i+1)种选择具体是哪些数,它取决于前面i-1位的数的选择情况
但是我们希望,这(n-i+1)种选择,最好是我们能够固定已知的,比如从0到(n-i);
到这我们就想到,不管这(n-i+1)种选择具体是哪些数,它们一定有着一个大小关系,最小的选择规定为0,最大的选择规定为(n-i)即可
根据这个规定,我们就可以引入次序数列了。

全排列与次序数列的一一对应

  • 例一:把全排列12345转换为次序数列
    第1位,有5种选择{1,2,3,4,5},选择了最小的数,1,所以次序数列的第1位为0;
    第2位,有4种选择{2,3,4,5},选择了最小的2,所以次序数列的第2位为0;
    以此类推……
    得到次序数列为00000;

  • 例二:把全排列54321转换为次序数列
    第1位,有5种选择{1,2,3,4,5},选择了最大的数,5,所以次序数列的第1位为4;
    第2位,有4种选择{1,2,3,4},选择了最大的数,4,所以次序数列的第2位为3;
    以此类推……
    得到次序数列为43210;

  • 例三:把全排列52413转换为次序数列
    第1位,有5种选择{1,2,3,4,5},选择了最大的数,5,所以次序数列的第1位为4;
    第2位,有4种选择{1,2,3,4},选择了第二大的数,2,所以次序数列的第2位为1;
    第3位,有3种选择{1,3,4},选择了最大的数,4,所以次序数列的第3位为2;
    以此类推……
    得到次序数列为41200;
    同理,给出一个次序数列,我们也能转换为唯一的全排列,下面只举一个例子

  • 例四:把次序数列33210转换为全排列
    第1位,有5种选择{1,2,3,4,5},次序数列第1位为3,说明选择了第四大的数,4,所以全排列的第1位为4;
    第2位,有4种选择{1,2,3,5},次序数列第2位为3,说明选择了第四大的数,5,所以全排列的第2位为5;
    以此类推……
    得到全排列为45321;

通过四个例子已经很清晰了这样的转换规则;
至于严谨的证明这样的一一映射,个人感觉没有必要(其实是懒了QAQ);感性上理解,在上述四个例子对每一位进行确定的过程中,都是唯一的。

次序数列、变进制数、康托展开

接着上面的四个例子,我们总结一下次序数列的性质:

  1. 第i位的取值范围是0到(n-i);
  2. 每一位都可以取遍它的取值范围中的所有值;
    (n!=n*(n-1)*……*1)
  3. 次序数列中更大的数对应的全排列也更大,相应的,次序也更大;

接下来,我们只需要建立次序数列和次序的一一映射,就可以建立全排列和次序的一一映射了;

下面想一个问题:什么是进制?

进制也就是进位计数制,是人为定义的带进位的计数方法,可以用有限的数字符号代表所有的数值。

对于我们要求出的“次序编号”来说,它就是我们日常中标准的十进制;既然次序和全排列/次序数列是一种一一映射的关系,而“次序编号”不过是一种计数方法,那么我们为什么不能把“次序数列”也当成一种等价的计数方法呢?
我们发现,次序数列的每一位都满足进制的要求,也就是,第i位为(n-i+1)进制,第i位用且仅能用0到(n-i)这(n-i+1)个数码表示。

下面我们来模拟第i位为(n-i+1)进制的变进制数十进制数之间的转换,这也就是次序数列和次序编号之间的转换。
还是以n=5为例:
次序编号为0,对应次序数列00000;
次序编号为1,在次序数列的最低位上加1,而第5位为1进制,所以进一位,得到次序数列00010;
次序编号为2,在次序数列的最低位上加1,第5位要进位,这时第4位变成2,而第4位为2进制,所以再进一位,得到次序数列00100;
次序编号为3,得到00110;
次序编号为4,得到00200;
次序编号为5,得到00210;
次序编号为6,再进一位,得到01000;
……
因为每一位能容纳的数有(n-i+1)个,所以在进位到第i位的时候,我们已经计了12……*(n-i)个数;
依此,我们就可以得到转换公式:

设次序数列的每一位为a1,a2,…,an,对应的次序编号为x,则有:
x=an * (n-1)! + an-1 * (n-2)! + …… + a2 * 1! + a1 * 0!

再多考虑一下,如何根据给出的全排列,直接得出次序数列(而不是像前面的例子一样,一位位找)?
先给出结论:

ai = 全排列中第i位数与它后面的每一位数字构成的逆序对的数目

很好想,对最后一位an,一定是剩下的一个数里面最小的,对应为0;对an-1,当时剩下两个数,如果它与an构成逆序对,说明它不是最小的,对应1,否则对应0;对an-2,当时剩下三个数,如果能与后两个数都构成逆序对,说明是最大的,对应2,以此类推……
上述两点就组成了我们说的康托展开,它构建了一个全排列与自然数的双射~

分析

说了那么多,回到这道题,我们需要哪些步骤?

  1. 把火星人给出的全排列转化为次序数列;
  2. 次序数列加上题干给出的m,并依据变进制数的进位规则进行进位;
  3. 把进位后的次序数列转化为新的全排列。

怎么解决步骤1?
首先,如果不考虑是否使用过,那么读入a[i],就是第a[i]大的数;
其次,考虑次序数列的定义,需要多开一个数组,记录1到n中哪些数字被用过;
比a[i]大的数字被用过,并不会对a[i]对应的次序数列的值产生影响;
只有比a[i]小的数字被用过,才会对a[i]对应的次序数列的值产生影响
每读入一个a[i],就扫描记录是否被用过的数组,下标从1到a[i]-1,用过,就对a[i]–;(注意多设一个临时变量,防止更改a[i]导致for循环中的范围被改变);最后把a[i]标记为用过,读入下一个;

步骤1完成之后,数组a储存的是次序数列。

怎么解决步骤2?
很类似高精度加法,只不过每一位的进位方式都不一样而已;
先把m全都加到最后一位;
用for循环来进位(已知不会溢出到更多位),从后往前进行;
对当前位,保留余数,往前一位进商;

怎么解决步骤3?
先清空记录用过的数组;
对于第i位,当前次序数列的值为a[i],我们对它的原始期望是a[i]+1;但是,从1到a[i]+1中,很可能会有用过的数,这个时候就应该调高对它的期望;
例如,对于次序数列的某个值1,本来只需要查看1到2是否有用过的,没有,那它在全排列中就是1+1=2;如果发现2用过,就让1++,变为2,查看3是否用过,没有,那它在全排列中就是2+1=3,不然,发现3也用过,再让2++,变为3,查看4是否有用过……

上代码

#include<iostream>
using namespace std;
const int maxn = 10000;
int a[maxn + 10], b[maxn + 10];
bool used[maxn + 10] = { 0 };
int m, n;
void step1()
{for (int i = 1; i <= n; i++){cin >> a[i];int tmp = a[i];for (int j = 1; j < a[i]; j++){tmp -= used[j];}used[a[i]] = 1;a[i] = tmp-1;}
}void step2()
{a[n] += m;for (int i = n; i > 1; i--){a[i - 1] += a[i] / (n - i + 1);a[i] %= n - i + 1;}
}void step3()
{memset(used, 0, sizeof(used));for (int i = 1; i <= n; i++){for (int j = 1; j <= a[i] + 1; j++)a[i] += used[j];cout << a[i] + 1 << ' ';used[a[i] + 1] = 1;}
}
int main()
{cin >> n >> m;step1();step2();step3();return 0;
}

洛谷P1088.火星人【模拟/搜索/康托展开】相关推荐

  1. [洛谷P5367]【模板】康托展开

    题目大意:给定一个$n$的排列,求它在$n$的全排列中的名次 题解:康托展开,对于一个全排列,第$i$为有$n+1-i$种选择,用变进制数表示,这一位就是$n+1-i$进制.记排列中第$[1,i)$中 ...

  2. 洛谷P1088 火星人

    题目描述 人类终于登上了火星的土地并且见到了神秘的火星人.人类和火星人都无法理解对方的语言,但是我们的科学家发明了一种用数字交流的方法.这种交流方法是这样的,首先,火星人把一个非常大的数字告诉人类科学 ...

  3. 洛谷 P1088 火星人

    题目描述 人类终于登上了火星的土地并且见到了神秘的火星人.人类和火星人都无法理解对方的语言,但是我们的科学家发明了一种用数字交流的方法.这种交流方法是这样的,首先,火星人把一个非常大的数字告诉人类科学 ...

  4. 洛谷P1088 火星人__(作业)

    非常水!!! next_permutation() 很优秀的函数!!! 题目点这里 1 #include<bits/stdc++.h> 2 using namespace std; 3 c ...

  5. 洛谷1088 火星人

    洛谷1088 火星人 题目描述 人类终于登上了火星的土地并且见到了神秘的火星人.人类和火星人都无法理解对方的语言,但是我们的科学家发明了一种用数字交流的方法.这种交流方法是这样的,首先,火星人把一个非 ...

  6. 洛谷 2921 记忆化搜索 tarjan 基环外向树

    洛谷 2921 记忆化搜索 tarjan 传送门 (https://www.luogu.org/problem/show?pid=2921) 做这题的经历有点玄学,,起因是某个random题的同学突然 ...

  7. 简单 洛谷 P1563 【模拟】玩具谜题普及场

    ** 简单 洛谷 P1563 [模拟]玩具谜题普及场** 小南有一套可爱的玩具小人, 它们各有不同的职业. 有一天, 这些玩具小人把小南的眼镜藏了起来. 小南发现玩具小人们围成了一个圈,它们有的面朝圈 ...

  8. 火星人(洛谷P1088题题解,C++语言描述)

    题目要求 题目链接 分析 STL有一个函数next_permutation(),直接给出下一个序列. 想自己做的话,建议学习康托展开. AC代码 #include <iostream> # ...

  9. 洛谷P1074 靶形数独 [搜索]

    题目传送门 题目描述 小城和小华都是热爱数学的好学生,最近,他们不约而同地迷上了数独游戏,好胜的他 们想用数独来一比高低.但普通的数独对他们来说都过于简单了,于是他们向 Z 博士请教, Z 博士拿出了 ...

最新文章

  1. C#技术内幕 学习笔记
  2. SWPU OnlingJudge 在线评测平台 使用教程
  3. 小功率电子镇流荧光灯相关实验
  4. python程序设计报告-20191206 实验二《Python程序设计》实验报告
  5. 服务器 操作系统安装到sd卡,DELL服务器通过sd卡安装系统(iDRAC Use vFlash ).doc
  6. python封装函数、实现将任意的对象序列化到磁盘上_Python系列之lambda、函数、序列化...
  7. 科大星云诗社动态20210329
  8. 【⌛工欲善其事,必先利其器⏳】葵花宝典の费曼学习法
  9. 生活中处处有joke!!
  10. “让Keras更酷一些!”:层中层与mask
  11. java 泛型的上限与下限、泛型通配符、泛型上下限
  12. Java记录 -73- 泛型详解
  13. 【POJ3608】Bridge Across Islands(旋转卡壳求两凸多边形的最短间距)
  14. 计算机数据库基础知识填空题,数据库练习题(基础)
  15. linux yum和apt,yum和apt-get用法及区别
  16. 遥感影像 全色 多光谱
  17. CSS 背景鼠标滑过,提示文字
  18. echarts is not defined
  19. V-REP:虚拟机器人实验平台
  20. jxls2.0的基础使用

热门文章

  1. 详解Paint的setXfermode(Xfermode xfermode)
  2. CH376的串口模式操作U盘(读、写、txt文件、csv文件、串口调试讲解、stm32程序)
  3. 金融行业云都有什么需求
  4. 2014小学计算机教师招聘笔试,2014江西教师招聘考试《小学信息技术》真题及答案解析.doc...
  5. “国防七校”之一西工大遭境外网络攻击
  6. 微淘百课微信万群直播定制机好用么?
  7. java中map嵌套map_java中遍历MAP,嵌套map的几种方法
  8. item2 + oh-my-zsh
  9. 安徽全省谷歌卫星地图免费下载的方法
  10. (一)大数据学习之shell脚本