Algorithm cho game kim cương

được bạn share cho cái bài tập
giống như game kim cương khi mà hàng ngang hàng dọc hoặc hàng chéo có 4+ viên đá cùng màu thì nó sẽ biến mất, mấy viên đá ở trên sẽ rơi xuống thế chỗ viên đá biến mất.
mỗi lần input thì có 1 viên đá rơi xuống bàn.
đề bài là từ 1 đống input cho ra output là hình dạng cuối của bàn chơi.

đề bài tiếng đức cho bạn nào muốn google translate
In dieser Aufgabe sollen Sie den Kern einer Computerspiel-Engine schreiben. Die Spiel-Mechanik
kann man sich als eine Mischung aus „Vier gewinnt“ und „Candy Crush“ vorstellen: Auf ein
diskretes Spielfeld Z × N0 fallen von oben (aus positiver y-Richtung) nacheinander einzelne
Spielsteine (nachfolgend „Steine“) unterschiedlicher Farben herunter und bilden Stapel. Sobald
vier oder mehr Steine der selben Farbe vertikal, horizontal oder diagonal in einer geraden Linie zusammenhängen, lösen diese sich auf; die darüber liegenden Steine rutschen dann nach
unten nach. Auch wenn ein Stein Teil zweier Viererlinien ist, lösen sich beide auf (z. B. wenn
sieben Steine T-förmig angeordnet sind oder sich zwei Linien in anderer Weise überschneiden).
Die Schritte des Auflösens und Nachrutschens werden wiederholt, bis keine Viererlinie gleicher
Steine mehr existiert. Erst dann kann der nächste Stein von oben in das Spielfeld hineinfallen.
Wenn durch das Nachrutschen mehrere Viererlinien gleichzeitig entstehen, so lösen sich diese
auch gleichzeitig auf.
Ihr Programm soll für eine gegebene Sequenz hereinfallender Steine die Spielmechanik simulieren und den resultierenden Endzustand auszugeben.
In dieser Aufgabe sollen Sie den Kern einer Computerspiel-Engine schreiben. Die Spiel-Mechanik
kann man sich als eine Mischung aus „Vier gewinnt“ und „Candy Crush“ vorstellen: Auf ein
diskretes Spielfeld Z × N0 fallen von oben (aus positiver y-Richtung) nacheinander einzelne
Spielsteine (nachfolgend „Steine“) unterschiedlicher Farben herunter und bilden Stapel. Sobald
vier oder mehr Steine der selben Farbe vertikal, horizontal oder diagonal in einer geraden Linie zusammenhängen, lösen diese sich auf; die darüber liegenden Steine rutschen dann nach
unten nach. Auch wenn ein Stein Teil zweier Viererlinien ist, lösen sich beide auf (z. B. wenn
sieben Steine T-förmig angeordnet sind oder sich zwei Linien in anderer Weise überschneiden).
Die Schritte des Auflösens und Nachrutschens werden wiederholt, bis keine Viererlinie gleicher
Steine mehr existiert. Erst dann kann der nächste Stein von oben in das Spielfeld hineinfallen.
Wenn durch das Nachrutschen mehrere Viererlinien gleichzeitig entstehen, so lösen sich diese
auch gleichzeitig auf.
Ihr Programm soll für eine gegebene Sequenz hereinfallender Steine die Spielmechanik simulieren und den resultierenden Endzustand auszugeben.

input: số đầu tiên là màu của viên đá, số thứ 2 là hoành độ x

1 0
1 1
1 2
0 3
0 -1
0 -2
0 -3
0 -3
3 -2
1 -1
2 0
2 1
2 2
3 3
3 2
3 1
3 0
2 -1
0 -2
0 -1
0 -4

output: màu, hoành độ, tung độ của các viên đá còn lại trên bàn

0 3 0
3 -2 0
3 0 0
3 1 0
3 2 0
3 3 1

constrain: 0 \le color \le 254, |x| \le 2^{20}

tạm thời thì mình đang brutal force, nhưng mà test lên tầm 25k input thì mất khoảng 10 phút

  • save bàn chơi vào 1 cái array 2 chiều
  • mỗi lần có viên đá mới vào thì kiểm tra toàn bộ array xem có chỗ nào có 4 viên cùng màu không
  • nếu có thì xóa xong đẩy mấy viên ở trên xuống

mình có thử chỉ kiểm tra chỗ viên đá mới vào với xung quanh viên đá bị xóa, nhưng mà kết quả không chính xác. mãi không nghĩ ra cách nhanh hơn nên lên đây hóng hớt

code của mình
from time import time

start = time()
field = []
offset = None
		
def push(col):
	count = 0
	for i in range(len(col)): 
		if col[i] != -1:
			col[count] = col[i] 
			count += 1
	while count < len(col): 
		col[count] = -1
		count += 1

def drop(field):
	found = False
	result = []
	for i in range(len(field)):
		for j in range(len(field[0])):
			if field[i][j] == -1:
				continue

			lines = [(i, j)]
			k = i + 1
			while k < len(field) and field[k][j] == field[i][j]:
				lines.append((k, j))
				k += 1
			if len(lines) > 3:
				result.append(lines)

			lines = [(i, j)]
			k = j + 1
			while k < len(field[0]) and field[i][k] == field[i][j]:
				lines.append((i, k))
				k += 1
			if len(lines) > 3:
				result.append(lines)

			lines = [(i, j)]
			w, h = i+1, j+1
			while w < len(field) and h < len(field[0]) and field[w][h] == field[i][j]:
				lines.append((w, h))
				w += 1
				h += 1
			if len(lines) > 3:
				result.append(lines)
			
			lines = [(i, j)]
			w, h = i+1, j-1
			while w < len(field) and h > -1 and field[w][h] == field[i][j]:
				lines.append((w, h))
				w += 1
				h -= 1
			if len(lines) > 3:
				result.append(lines)

	pushed = set()
	for line in result:
		for pos in line:
			field[pos[0]][pos[1]] = -1
			pushed.add(pos[0])
	for col in pushed:
		push(field[col])
	if result:
		found = True
	return found

with open("c:\\Users\\tungs\\Downloads\\extra\\test.txt", "r") as f:
	for line in f:
		data = tuple(map(int, line.split()))
		if offset is None:
			field.append([data[0]])
			offset = data[1]
		else:
			if data[1] - offset + 1 > len(field):		
				field += [[-1 for _ in range(len(field[0]))] for _ in range(data[1] - offset + 1 - len(field))]
			if offset > data[1]:
				field = [[-1 for _ in range(len(field[0]))] for _ in range(offset - data[1])] + field
				offset = data[1]
			if field[data[1] - offset][-1] != -1:
				[f.append(-1) for f in field]
			for i in range(len(field[0])):
				if field[data[1] - offset][i] == -1:
					field[data[1] - offset][i] = data[0]
					while drop(field):
						pass	
					break

result = []
for i in range(len(field)):
	for j in range(len(field[i])):
		if field[i][j] != -1:
			result.append((field[i][j], i+offset, j))
result.sort()
for cell in result:
	print(*cell)
	
print(len(result), time()-start)
3 Likes

Như bạn có nói

Vốn dĩ muốn nhanh thì cách này mới giải quyết được, chỉ cần cẩn thận là được.

Khi 1 dãy các viên đá liên tiếp (theo hàng ngang/dọc/chéo):

  • Các viên đá ở trên sẽ rơi xuống, tạo nên bảng đá mới.
  • Với các viên đá vừa mới rơi, kiểm tra xem có dãy viên đá liên tiếp nào mới được tạo thành hay không.

Tuy nhiên, nếu kiểm tra tất cả các viên đá vừa mới rơi thì quá lâu (vì không cần thiết).

Ví dụ, 1 dãy đá liên tiếp x1, x2, x3, x4 theo đường chéo lên bị xoá:

image

Các viên đá ở cột 1, 2, 3, 4 kể từ vị trí A1, A2, A3, A4 bị “tụt” xuống 1 hàng. Do vậy chỉ có các viên đá ở vị trí x1, x2, x3, x4 (cũ) và các ô ở cột 1 sau A1, cột 4 sau A4 mới cần thay đổi trạng thái 8 ô kề nó.

Một ví dụ khác, 1 dãy đá liên tiếp y1, y2, y3, y4 theo hàng dọc lên bị xoá:

image

Toàn bộ các viên đá ở cột 2, kể từ vị trí của A2 sẽ thay đổi trạng thái của 6 ô kề 2 bên của nó.

Từ ví dụ có thể thấy rằng, sau 1 lần xoá 1/các dãy viên đá liên tiếp:

  • Với 1 số viên đá, nó sẽ bị “tụt” theo 1 số hàng nào đó. Số hàng bị tụt của viên đá tại ô (i, j) = tổng số ô bị xoá trong cột j và hàng i’ < i.
  • Các viên đá ở biên của khối đá bị “tụt” sẽ thay đổi trạng thái các ô kề.

Mấu chốt ở đây là ta phải biết được viên nào bị xoá, viên nào chưa bị xoá. Nếu xoá “chay”, xoá và cập nhật lại bảng đá liên tục thì độ phức tạp mỗi lần xoá ít nhất là O(mn), toàn bộ bài toán sẽ giải quyết trong thời gian cỡ O(m^2 n^2) (quét từng ô, mỗi lần phát hiện có hàng liên tiếp thì xoá và cập nhật lại bảng).

Cách của mình đưa ra, đó là cập nhật lại vị trí của các viên đá nhưng không thực sự xoá bảng mà “trỏ” từ vị trí thật đến vị trí ban đầu của mỗi viên đá trong bảng.

Gọi index_map là 1 cấu trúc dữ liệu quản lý các viên đá theo mỗi cột, chỉ số hàng sẽ đánh ngược, dòng dưới cùng là dòng 0, dòng trên dòng 0 là dòng 1,…

Ban đầu index_map[j][i] := i. Sau mỗi bước cập nhật, đối với viên đá ở ô (i, j) mà không bị xoá:

index_map[j][i] := index_map[j][i] - Số hàng bị tụt của viên đá tại ô (i, j) 
                := index_map[j][i] - tổng số ô bị xoá trong cột j và hàng i' < i

Bên cạnh đó, sau mỗi bước xoá, nếu cột j có x viên đá bị xoá thì index_map[j] pop bớt đi x phần tử đuôi.

Ví dụ:

image

Sau khi xoá lần 1, index_map[0][1] = 2, tức là viên đá ở cột 0, hàng 1 hiện tại vốn là viên đá ở vị trí cột 0, hàng 2.

Kiểm tra dãy đá liên tiếp vẫn rất đơn giản, \displaystyle new\_table[i_1][j_1] = new\_table[i_2][j_2]\\\Leftrightarrow origin\_table[i_1'][j_1] = origin\_table[i_2'][j_2]

với \displaystyle i_1' = index\_map[j_1][i_1], i_2' = index\_map[j_2][i_2].

Từ ô (i, j), ta dễ dàng loang ra 8 hướng để tìm kiếm những ô kề nó và có tiềm năng bị xoá, kiểm tra có 4+ viên đá hay không rồi cập nhật lại bảng index_map.

Độ phức tạp có lẽ sẽ giảm đi đôi chút. Tuy nhiên đây chỉ là thuật toán còn sơ khai của mình, có thể vẫn rất khó hiểu và chưa tối ưu được ngay, mong được các bạn đóng góp thêm.


Hướng tối ưu tiếp tục của mình là sau khi viên đá ở (i_1, j_1)(i_2, j_2) bị “tụt” thành (i_1', j_1)(i_2', j_2), tiếp tục kiểm tra từ viên đá (i_1', j_1), cuối cùng có thể tạo thành 1 graph với các đỉnh dạng (i, j).

10 Likes

Em có 1 ý tưởng có lẽ là rút ngắn hơn được thời gian nhưng sẽ tăng độ lằng nhằng của code vì phải thêm 1 số điều kiện. Ý tưởng của em như sau

Do phải kiểm tra 4 viên liên tiếp nên khi kiểm tra hàng 4 từ trên xuống chắc chắn sẽ kiểm tra cả đường chéo và hàng dọc của 3 hàng trên => nếu là 3 hàng trên cùng thì chỉ cần kiểu tra bề ngang. Suy luận tương tự ta có được chỉ cần kiểm tra bề ngang của 3 cạnh từ trên xuống và 3 cạnh từ dưới lên, chỉ kiểm tra bề dọc của 3 cạnh từ trái sang và 3 cạnh từ phải sang.

Nếu giả sử Box 10 x 10 ta sẽ phải kiểm tra 800 hàng(chéo, ngang, dọc). Nếu cách trên đúng thì sẽ giảm được (60 + 24) * 8 - 48 lần kiểm tra. Một con số đáng để xem xét.

2 Likes

Ý tưởng của bạn không xử lý được trường hợp các viên đá biến mất liên tục. Ví dụ sau khi 1 hàng chéo biến mất thì 3 hàng ngang sẽ xảy ra.
image
Trong ví dụ này, khi 1 viên kim cương C rơi vào ô Blue thì sau đó 3 dòng: 3, 4, 5 sẽ bị biến mất sau khi đường chéo C biến mất (Tương ứng với 4 viên C, 4 viên B và 4 viên A).

Ở đây mình thấy cách tối ưu vẫn là kiểm tra những viên đá mới được di chuyển. Cụ thể là nên sử dụng queue hoặc stack để lưu lại các vị trí cần check:

  • Step 1. Check G2 -> Found Diag line C [G2, F3, E4, D5]
    a. Remove G2, check if need to move other cells -> No
    b. Remove F3, check if need to move other cells -> No
    c. Remove E4, check if need to move other cells -> Yes -> Move [E3] to [E4], Push E4 to stack
    d. Remove D5, check if need to move other cells -> Yes -> Move move [D4, D3, D2] to [D5, D4, D3] -> Push to stack [D5, D4, D3]
  • Step 2. Check stack [E4, D5, D4, D3] -> pop first element then repeat step 1.
7 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?