Hackerank-Array-NewYearChaos
题目背景描述
新年第一天,N 个人排队坐过山车。每个人穿有带编号的衣服 \([1, 2, 3, ...]\)。
因为排队时间太久,有人发现给前面相邻的人喂一颗糖,就可以和他交换位置,而每人手里只有两颗糖。所以这些人就通过“喂糖”的方式,让队伍变得乱七八糟。
作为游乐场管理员,为了纠正这种不正之风,你要使队伍恢复原状。首先,你的任务是:要确定需要最少交换多少次才能让队伍恢复原状。
要求
编写函数,计算最少交换次数;若所给数列不是由上述规则打乱的,则显示"Too chaos!"
输入:整形数组 q[n]
-> 乱序队列
输出 :
- 打印整数
candies
-> 交换所需的糖果数量 - 或
"Too chaos!"
-> 提示输入无效
样例
输入数列1:[3, 2, 5, 6, 7, 4, 1]
恢复过程如下:
- 7 后退两格:
[3, 2, 5, 6, 4, 1, 7]
- 6 后退两格:
[3, 2, 5, 4, 1, 6, 7]
- 5 后退两格:
[3, 2, 4, 1, 5, 6, 7]
- 4 后退一格:
[3, 2, 1, 4, 5, 6, 7]
- 3 后退两格:
[2, 1, 3, 4, 5, 6, 7]
- 2 后退一格:
[1, 2, 3, 4, 5, 6, 7]
输入样例2:[3, 2, 5, 7, 6, 4, 1]
所给数列不是由上述规则所打乱的,因为一个人只有两颗糖,7 不可能给 3 个人“喂糖”。
思路
直观思路
类似冒泡排序,找到最大的数字并最多与后面的数字交换两次。如果达不到原来的位置则判定输入无效;否则固定排好的数字顺序,继续下一次“冒泡”。
时间复杂度分析:每次循环都要遍历数列找到最大数字并交换到末尾,一共循环 n 次:\(O(n^2)\)
当数据量达到 \(n^5\) 量级时,计算量达到 \(n^{10}\),测试提示超时。
优化思路
无效输入的判断
以之前样例中的输入为例:
[3, 2, 5, 6, 7, 4, 1]
目标结果,亦即原来的队形为:
[1, 2, 3, 4, 5, 6, 7]
将两数列相减,得到每个人从初始位置来到当前位置所需要移动的次数,亦即需要消耗的糖果个数(负数实际上是表示吃到糖果的个数):
[2, 0, 2, 2, 2, -2, -6]
只要两个数列相减得到的差超过每个人手里糖的个数,则该数列无效。
最少交换次数的计算
刚才提到,两数列之差里面的每个正数,是所对应的人从初始位置来到当前位置所需要消耗的糖果个数,但是否意味着我把得到的差的数列里面的正数加起来就是所需要的交换次数呢?
答案是否定的。
为什么呢?
因为两数列之差,实质上仅仅是考虑了每个人从初始位置来到当前位置所需要单独移动的步数,而并没有考虑和别人交换给别人带来的影响。因此每进行一次交换,这个差值都可能有所变化。
所以差值数列对于我们的计算已经没有帮助了,我们需要找到其它信息帮助计算最少交换次数。
再审一遍题目,我们会发现,唯一的限制条件就是:在原队形中,某个人最多只能和他前面的两个人交换位置,因为每个人只有两块糖。
回到之前的例子,仔细观察第一个样例:
输入数列1:[3, 2, 5, 6, 7, 4, 1]
- 7 后退两格:
[3, 2, 5, 6, 4, 1, 7]
- 6 后退两格:
[3, 2, 5, 4, 1, 6, 7]
- 5 后退两格:
[3, 2, 4, 1, 5, 6, 7]
- 4 后退一格:
[3, 2, 1, 4, 5, 6, 7]
- 3 后退两格:
[2, 1, 3, 4, 5, 6, 7]
- 2 后退一格:
[1, 2, 3, 4, 5, 6, 7]
7 之所以能够后退两格,是因为 7 的后面有两个数字比 7 小;
6 之所以能够后退两格,是因为 6 的后面有两个数字比 6 小;
5 之所以能够后退两格,是因为 6 的后面有两个数字比 5 小;
4 之所以能够后退一格,是因为 4 的后面有一个数字比 4 小;
3 之所以能够后退两格,是因为 3 的后面有两个数字比 3 小;
2 之所以能够后退一格,是因为 2 的后面有两个数字比 2 小;
而且,交换数字使序列回归部分有序状态,并不影响乱序部分的队形。也就是说,可以统计每个数字后面比该数字小的数字个数,然后对每个数字的统计结果求和。
但是,上述算法需要对每个数字遍历进行统计,依然是 \(O(n^2)\) 的时间复杂度。
换个方向,我们也可以这样做:
输入数列1:[3, 2, 5, 6, 7, 4, 1]
- 1 前进六格:
[1, 3, 2, 5, 6, 7, 4]
- 2 前进一格:
[1, 2, 3, 5, 6, 7, 4]
- 4 前进三格:
[1, 2, 3, 4, 5, 6, 7]
1 之所以能够前进六格,是因为 1 的前面有六个数字比 1 大;
2 之所以能够前进一格,是因为 2 的前面有一个数字比 2 大;
4 之所以能够前进三格,是因为 4 的前面有三个数字比 4 大;
( 注意:“被喂糖”的次数是没有硬性规定的)
从右往左搜索比当前数字大的数,搜索范围就被“喂糖次数限制”限定在了一定范围内。
比如说,我要找比 6 大的数,比 6 大的数只可能出现在原队形中 6 的位置左边两格以右的区域:
[3, 2, 5, 6, 7, 4, 1]
[1, 2, 3, 4, 5, 6, 7]
在这个例子中,比 6 大的数只可能出现在第 5 个数字及以后,而当前的乱序队形中 6 是第 4 个数字,也就是说在 6 的左边且比 6 大的数字不存在,也就不用找了。
更严谨的语言表达就是:
设当前乱序队形为 \(Q\),\(Q_i\) 表示队列中的第 \(i\) 个数字,\(1\le i \le n\) 也可以代表原队形;
如果要找在 \(Q_i\) 左边且比 \(Q_i\) 大的数字,只需要在 \(j = max(Q_i-1, 0), ..., i-1\) 的取值范围内遍历 \(Q_j\) 就能找到。
实现
def minimumBribes(q):
candies = 0
for i in range(len(q)):
if q[i] - (i+1) > 2:
print('Too chaotic')
return
for j in range(max(0, q[i]-1-1), i):
if q[j] > q[i]:
candies += 1
print(candies)
Written with StackEdit.