问题描述
一些不同颜色的球排成一行。当形成连续的三个或更多相同颜色的球块时,将其从线中移除。在这种情况下,所有的球都相互移动,这种情况可能会重复。
编写一个函数lines(a) 来确定有多少球将被销毁。初始时刻最多可以有一个连续的三个或三个以上相同颜色的球块。
输入数据:
该函数采用带有初始球配置的列表 a。球数小于等于1000,球的颜色可以从0到9,每种颜色都有自己的整数。
输出数据:
Input:[2,2,1,1] Output:6
input : [0,0],output: 5
input:[2,3,4],output: 0
我尝试使用双指针方法,但不知道该怎么做。
def lines(a):
left = 0
right = len(a)-1
s=[]
while left < right :
if a[left] == a[left:right+1]:
s.extend(a[left: right+1])
del a[left: right+1]
else:
left += 1
right -= 1
return len(s)
test_a = [2,1]
print(lines(test_a))
我认为 if a[left] == a[left:right+1]:
不起作用,我尝试从左到右比较元素与从右到左的元素相同。 del a[left: right+1]
也不起作用,我尝试删除那些已经扩展到新列表的元素。
感谢您的建议。
解决方法
您可以为此使用堆栈(简单的列表实现)。检查最后3个球是否相同。如果是,则继续弹出所有相同类型的球。否则,将球添加到堆栈中并继续。每个球可以添加一次,删除一次到列表中,所以复杂度也应该是 O(N)。
最后销毁的球数=原球数-堆叠长度。
我们需要计算输入列表中出现的次数。
如果当前球颜色 (element
) 等于前一个球颜色 (previous
),则将计数器加一。
for i,element in enumerate(a):
if element == previous:
counter += 1
如果它们不相等,那么下一个颜色可能会重复超过三个。因此存储当前颜色的索引
for i,element in enumerate(a):
if element == previous:
counter += 1
else:
counter = 1
previous = element
previous_index = i
现在检查颜色是否重复超过 3 次。
如果是,我们需要从列表中删除球。
在移除的同时,我们还需要计算被破坏的球的数量。
if counter >= 3:
for _ in range(previous_index,i+1):
a.pop(previous_index)
total_destroyed += 1
可能会有混淆,我为什么要a.pop(previous_index)
?
如果你在一个例子上调试这部分代码。例如:[2,2,1,1]
当i
== 4时,当前列表为[2,1]
,满足count >= 3
- Iteration = 1,列表会变成
[2,1]
如果你说删除下一个元素,这将弹出最后一个元素
- Iteration = 2,列表会变成
[2,1]
现在,在迭代 3 中,哪个元素将被弹出?索引越界。
- Iteration = 3,列表不会改变
[2,1]
因此,在迭代过程中总是弹出当前元素。因为下一个元素将是当前元素。
现在我们需要再次调用该方法,看看是否还有剩余的球
if counter >= 3:
for _ in range(previous_index,i+1):
a.pop(previous_index)
total_destroyed += 1
lines(a)
但是,我们必须小心,因为我们已经将 previous
、previous_index
、counter
和 totally_destroyed
声明为局部变量。
如果我们将它们保留为局部属性,所有变量都将被重新初始化,因此算法结果将不成立。
因此我们必须将它们初始化为一个全局变量并返回被破坏的球的总数。
代码:
total_destroyed = 0
counter = 0
previous = -1
previous_index = -1
def lines(a):
"""
Args:
a (list): input array
Returns:
(int): total number of destroyed balls.
"""
global total_destroyed
global counter
global previous
global previous_index
for i,element in enumerate(a):
if element == previous:
counter += 1
else:
counter = 1
previous = element
previous_index = i
if counter >= 3:
for _ in range(previous_index,i+1):
a.pop(previous_index)
total_destroyed += 1
lines(a)
return total_destroyed
test_a = [2,1]
test_b = [0,0]
test_c = [2,3,4]
print(lines(test_a))
total_destroyed = 0
counter = 0
previous = -1
previous_index = -1
print(lines(test_b))
total_destroyed = 0
counter = 0
previous = -1
previous_index = -1
print(lines(test_c))
结果:
6
5
0
,
我的解决方案有两个部分。按球的编号 grouping_balls
分组的一种方法。然后递归方法首先检查是否仍然存在大于 3 的组,如果是,则销毁它们并将其余的合并用于下一次迭代 destroy_balls
。
import itertools
# Split balls by type
# For 2211121 you would get: ["22","111","2","1"]
def grouping_balls(balls):
return ["".join(g) for k,g in itertools.groupby(balls)]
# Keeps destroying balls as long as there are groups of 3 or more
def destroy_balls(list_balls,destroyed):
if len(list_balls) < 1:
return destroyed
balls_grp = grouping_balls(list_balls)
# No more balls to destroy
if max(map(len,balls_grp)) < 3:
return destroyed
# Destroying and merging balls
else:
non_dest_balls = ""
for l in balls_grp:
if len(l) < 3:
non_dest_balls += l
else:
destroyed += len(l)
return destroy_balls(non_dest_balls,destroyed)
Input = [0,0]
destroy_balls(''.join(map(str,Input)),0)
,
这是一个使用 lo
和 hi
指针遍历输入的迭代解决方案。
请注意,通过附加一个保证不是作为输入颜色的结束标记意味着检查颜色运行的逻辑不需要在 while 循环之外重复。
代码
in_outs = [([2,1],6),([0,0],5),([2,4],0),]
def test(f):
print(f"\nSTART Testing answer {f.__doc__}")
for arg,ans in in_outs:
try:
out = f(arg.copy())
except:
ans = '<Exception thrown!>'
if out != ans:
print(f" {f.__name__}({arg}) != {ans} # instead gives: {out}")
else:
print(f" {f.__name__}({arg}) == {out}")
print(f"STOP Testing answer {f.__doc__}\n")
#%% From me,Paddy3118
def lines(a):
"From Paddy3118"
a = a.copy() + [-1] # Add terminator
d = lo = hi = 0 # delete count,lo & hi pointers
lo_c = hi_c = a[0] # Colours at pointer positions
while hi +1 < len(a):
hi += 1; hi_c = a[hi]
if lo_c != hi_c:
if hi - lo > 2:
d += hi - lo
del a[lo: hi]
lo,hi,lo_c,hi_c = 0,a[0],a[0]
else:
lo,lo_c = hi,hi_c
return d
test(lines)
输出
START Testing answer From Paddy3118
lines([2,1]) == 6
lines([0,0]) == 5
lines([2,4]) == 0
STOP Testing answer From Paddy3118
检查其他示例
使用以下工具扩展上述内容以辅助测试
#%% IA from Ignacio Alorre
import itertools
def grouping_balls(balls):
return ["".join(g) for k,g in itertools.groupby(balls)]
def destroy_balls(list_balls,destroyed)
def lines_IA(a):
"From Ignacio Alorre"
return destroy_balls(''.join(map(str,a)),0)
test(lines_IA)
#%% AHX from Ahx
total_destroyed = 0
counter = 0
previous = -1
previous_index = -1
def _lines_ahx(a):
"""
Args:
a (list): input array
Returns:
(int): total number of destroyed balls.
"""
global total_destroyed
global counter
global previous
global previous_index
for i,i+1):
a.pop(previous_index)
total_destroyed += 1
_lines_ahx(a)
return total_destroyed
def lines_AHX(a):
"From Ahx"
global total_destroyed
global counter
global previous
global previous_index
total_destroyed = 0
counter = 0
previous = -1
previous_index = -1
return _lines_ahx(a)
test(lines_AHX)
完整输出
所有三个示例都适用于给定的测试。由于测试非常小,因此没有给出时间。
START Testing answer From Paddy3118
lines([2,4]) == 0
STOP Testing answer From Paddy3118
START Testing answer From Ignacio Alorre
lines_IA([2,1]) == 6
lines_IA([0,0]) == 5
lines_IA([2,4]) == 0
STOP Testing answer From Ignacio Alorre
START Testing answer From Ahx
lines_AHX([2,1]) == 6
lines_AHX([0,0]) == 5
lines_AHX([2,4]) == 0
STOP Testing answer From Ahx
,
我想到了另一种算法,在使用单个指针遍历列表并删除内容之前,首先对整个输入进行运行长度编码。它在更大的输入上效果更好。
为了测试它,我编写了一个例程 downto_zero
,它生成最终都被删除的 1 和 0 序列。
我通过定时运行所有示例来跟进。
注意:在 Spyder IDE 的 Ipython shell 中运行以进行计时。
注意:Ahx 的示例充分利用了递归,但输入长度为 12 时失败,更不用说 7500。
RLE 代码和测试生成器
#%% Test Generator for long tests
in_outs = [([2,]
import sys
def test(f,in_outs=in_outs):
print(f"\nSTART Testing answer {f.__doc__}")
for arg,ans in in_outs:
arg_txt = arg if len(arg) <= 8 else f"[<{len(arg)} terms>]"
try:
out = f(arg.copy())
except:
out = f'<Exception thrown! {sys.exc_info()[0]}>'
if out != ans:
print(f" {f.__name__}({arg_txt}) != {ans} # instead gives: {out}")
else:
print(f" {f.__name__}({arg_txt}) == {out}")
print(f"STOP Testing answer {f.__doc__}\n")
def downto_zero(n=3):
"Test generator that reduces all input"
if n == 0:
return []
x = ([0,1] * ((n+1)//2))[:n] # 0,1 ... of length n
e = x[-1] # end item
ne = 0 if e == 1 else 1 # not end item
r = ([e,e,ne,ne] * n)[:2*n]
return x + r
#%% RLE Runlengh encoded from me,Paddy
from itertools import groupby
def lines_RLE(a):
"From Paddy3118 using run-length encoding"
a = a.copy() + [-1] # Add terminator
a = [[key,len(list(group))] for key,group in groupby(a)] # RLE
d = pt = 0 # delete count,pointer
while pt +1 < len(a):
i0,n0 = a[pt] # item,count at pt
if n0 > 2:
d += n0
del a[pt]
if pt > 0:
if a[pt - 1][0] == a[pt][0]: # consolidate
a[pt - 1][1] += a[pt][1]
del a[pt]
pt -= 1
continue
else:
pt += 1
return d
test(lines_RLE,in_outs)
#%% Timed testing
print("TIMED TESTING\n=============")
n = 2_500
for f in (lines,lines_RLE,lines_IA,lines_AHX):
dn = downto_zero(n)
%time test(f,[(dn,len(dn))])
定时输出
START Testing answer From Paddy3118 using run-length encoding
lines_RLE([2,1]) == 6
lines_RLE([0,0]) == 5
lines_RLE([2,4]) == 0
STOP Testing answer From Paddy3118 using run-length encoding
TIMED TESTING
=============
START Testing answer From Paddy3118
lines([<7500 terms>]) == 7500
STOP Testing answer From Paddy3118
Wall time: 2.44 s
START Testing answer From Paddy3118 using run-length encoding
lines_RLE([<7500 terms>]) == 7500
STOP Testing answer From Paddy3118 using run-length encoding
Wall time: 19 ms
START Testing answer From Ignacio Alorre
lines_IA([<7500 terms>]) == 7500
STOP Testing answer From Ignacio Alorre
Wall time: 10.9 s
START Testing answer From Ahx
lines_AHX([<7500 terms>]) != 7500 # instead gives: <Exception thrown! <class 'RecursionError'>>
STOP Testing answer From Ahx
Wall time: 16 ms
,
我的第三个也是最后一个答案建立在我的第二个 RLE 条目上,通过交换使用双向链表数据结构来删除可能代价高昂的数组删除操作。与我的其他两个解决方案相比,新代码看起来具有更好的大 O 特性,因此对于更大的输入可能会更快。
双向链表代码:
#%% LL Linked-list and Runlengh encoded from me,Paddy
from itertools import groupby
def lines_LL(a):
"From Paddy3118 using a linked-list and run-length encoding"
a = a.copy() + [-1] # Add terminator
# a is list of [item,reps,prev_pointer,next_pointer]
a = [[key,len(list(group)),i - 1,i + 1]
for i,(key,group) in enumerate(groupby(a))] # linke-list RLE
a[0][-2] = None # No previous item
a[-1][-1] = None # No next item
d = pt = 0 # delete count,pointer
while pt is not None:
i0,n0,pre_pt,nxt_pt = a[pt] # item,count,next at pt
if n0 > 2:
d += n0
deleted_pt = pt
if pre_pt is not None:
a[pre_pt][-1] = pt = nxt_pt # del a[pt] & pt to next
if a[pre_pt][0] == a[pt][0]: # consolidate same items in pre
a[pre_pt][1] += a[pt][1]
a[pre_pt][-1] = a[pt][-1] # del a[pt]
pt = pre_pt # ... & pt to previous
else:
pt = nxt_pt
else:
pt = nxt_pt
return d
print("SIMPLE FUNCTIONAL TESTING\n=============")
test(lines_LL,in_outs)
#%% Timed testing
if 1:
print("TIMED TESTING\n=============")
n = 20_000
for f in (lines_LL,lines): #,lines_AHX):
dn = downto_zero(n)
%time test(f,len(dn))])
输出和时间
SIMPLE FUNCTIONAL TESTING
=============
START Testing answer From Paddy3118 using a linked-list and run-length encoding
lines_LL([2,1]) == 6
lines_LL([0,0]) == 5
lines_LL([2,4]) == 0
STOP Testing answer From Paddy3118 using a linked-list and run-length encoding
TIMED TESTING
=============
START Testing answer From Paddy3118 using a linked-list and run-length encoding
lines_LL([<60000 terms>]) == 60000
STOP Testing answer From Paddy3118 using a linked-list and run-length encoding
Wall time: 104 ms
START Testing answer From Paddy3118 using run-length encoding
lines_RLE([<60000 terms>]) == 60000
STOP Testing answer From Paddy3118 using run-length encoding
Wall time: 387 ms
START Testing answer From Paddy3118
lines([<60000 terms>]) == 60000
STOP Testing answer From Paddy3118
Wall time: 2min 31s