File size: 21,810 Bytes
f59f0fd
 
 
6850fea
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
 
 
 
 
f59f0fd
 
 
 
 
 
6850fea
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
f59f0fd
 
6850fea
f59f0fd
6850fea
f59f0fd
6850fea
f59f0fd
6850fea
 
 
 
 
 
f59f0fd
6850fea
f59f0fd
 
6850fea
 
 
f59f0fd
6850fea
 
 
 
f59f0fd
6850fea
 
 
 
 
f59f0fd
6850fea
f59f0fd
 
6850fea
 
 
f59f0fd
6850fea
 
 
 
 
f59f0fd
6850fea
 
f59f0fd
6850fea
 
f59f0fd
6850fea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f59f0fd
6850fea
f59f0fd
6850fea
 
 
f59f0fd
6850fea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f59f0fd
 
6850fea
 
 
f59f0fd
6850fea
 
 
f59f0fd
6850fea
f59f0fd
6850fea
 
 
 
 
 
 
 
 
f59f0fd
6850fea
f59f0fd
6850fea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f59f0fd
 
6850fea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f59f0fd
6850fea
f59f0fd
6850fea
 
f59f0fd
 
 
 
6850fea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f59f0fd
6850fea
 
f59f0fd
6850fea
 
f59f0fd
6850fea
 
 
 
 
f59f0fd
6850fea
 
 
 
 
 
 
 
 
f59f0fd
 
 
 
6850fea
f59f0fd
6850fea
f59f0fd
 
 
 
 
 
 
6850fea
f59f0fd
 
 
 
 
6850fea
f59f0fd
 
 
 
 
 
6850fea
f59f0fd
6850fea
f59f0fd
 
 
 
6850fea
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
 
 
 
 
 
 
 
 
 
 
 
f59f0fd
 
 
 
6850fea
f59f0fd
6850fea
f59f0fd
 
6850fea
 
 
f59f0fd
 
6850fea
f59f0fd
 
 
 
6850fea
f59f0fd
 
 
 
 
6850fea
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
f59f0fd
 
 
 
 
6850fea
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
f59f0fd
 
 
 
 
6850fea
f59f0fd
 
 
6850fea
f59f0fd
 
6850fea
f59f0fd
6850fea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f59f0fd
 
6850fea
f59f0fd
6850fea
 
 
 
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6850fea
 
 
f59f0fd
 
 
 
6850fea
f59f0fd
6850fea
f59f0fd
 
 
6850fea
 
 
 
f59f0fd
6850fea
 
 
 
f59f0fd
6850fea
f59f0fd
6850fea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f59f0fd
 
 
6850fea
f59f0fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
# algorithm_2d_1.py

from collections import deque
import heapq

from utils import (
    to_tuple_set,
    to_list,
    is_connected,
    is_move_connected,
    potential,
    apply_move,
    make_step_info,
    build_response,
    is_finished_2d
)


# ============================================================
# 1. Hình học move 2D: slide và convex transition
# ============================================================

def is_slide_2d(c, e, blocks_set):
    """
    Slide trong 2D dựa trên 4-cycle.

    c và e phải kề cạnh.
    Tồn tại một phía của cạnh c-e sao cho hai ô còn lại của hình vuông 2x2 đều có block.
    """
    dx = e[0] - c[0]
    dy = e[1] - c[1]

    if abs(dx) + abs(dy) != 1:
        return False

    # Hai vector vuông góc với hướng di chuyển
    p1 = (-dy, dx)
    p2 = (dy, -dx)

    side1_a = (c[0] + p1[0], c[1] + p1[1])
    side1_b = (e[0] + p1[0], e[1] + p1[1])

    side2_a = (c[0] + p2[0], c[1] + p2[1])
    side2_b = (e[0] + p2[0], e[1] + p2[1])

    if side1_a in blocks_set and side1_b in blocks_set:
        return True

    if side2_a in blocks_set and side2_b in blocks_set:
        return True

    return False


def is_convex_2d(c, e, blocks_set):
    """
    Convex transition trong 2D.

    c và e nằm chéo nhau trong một hình vuông 2x2.
    Trong hai ô còn lại, đúng một ô có block làm điểm tựa.
    """
    dx = abs(c[0] - e[0])
    dy = abs(c[1] - e[1])

    if dx != 1 or dy != 1:
        return False

    n1 = (c[0], e[1])
    n2 = (e[0], c[1])

    in_n1 = n1 in blocks_set
    in_n2 = n2 in blocks_set

    return in_n1 ^ in_n2


def get_move_type_2d(c, e, blocks_set):
    if is_slide_2d(c, e, blocks_set):
        return "slide"

    if is_convex_2d(c, e, blocks_set):
        return "convex"

    return None


def bounding_search_area_2d(blocks_set, margin=2):
    min_x = min(x for x, y in blocks_set) - margin
    max_x = max(x for x, y in blocks_set) + margin
    min_y = min(y for x, y in blocks_set) - margin
    max_y = max(y for x, y in blocks_set) + margin

    return min_x, max_x, min_y, max_y


def generate_valid_moves_2d(blocks_set):
    """
    Sinh tất cả move hợp lệ quanh cấu hình hiện tại.

    Điều kiện:
    - ô đích trống
    - không dùng tọa độ âm
    - là slide hoặc convex
    - C \ {c} liên thông
    - cấu hình sau move liên thông
    """
    moves = []

    if not blocks_set:
        return moves

    min_x, max_x, min_y, max_y = bounding_search_area_2d(blocks_set, margin=2)

    empty_cells = [
        (x, y)
        for x in range(min_x, max_x + 1)
        for y in range(min_y, max_y + 1)
        if (x, y) not in blocks_set and x >= 0 and y >= 0
    ]

    for c in blocks_set:
        remaining = set(blocks_set)
        remaining.remove(c)

        if not is_connected(remaining, 2):
            continue

        for e in empty_cells:
            move_type = get_move_type_2d(c, e, blocks_set)

            if move_type is None:
                continue

            if is_move_connected(blocks_set, c, e, 2):
                moves.append((c, e, move_type))

    return moves


# ============================================================
# 2. Đánh giá cấu hình: potential + holes + compact target
# ============================================================

def cell_potential_2d(cell):
    """
    Potential của một ô 2D, coi z = 0.
    """
    x, y = cell

    if y > 1:
        w = 3
    elif y == 1:
        w = 2
    else:
        w = 1

    return w * (x + 2 * y)


def finished_holes_2d(blocks_set):
    """
    Trả về tập các ô còn thiếu để mọi block hiện tại trở thành finished.

    Nếu có block (x,y), thì mọi ô trong hình chữ nhật
    {0..x} x {0..y} phải có mặt.
    """
    holes = set()

    for x, y in blocks_set:
        for i in range(x + 1):
            for j in range(y + 1):
                if (i, j) not in blocks_set:
                    holes.add((i, j))

    return holes


def compact_target_2d(n):
    """
    Tạo một cấu hình target compact gồm n ô.

    Đây là down-set gần gốc, được xây bằng cách thêm dần ô có potential nhỏ nhất
    mà vẫn giữ tính finished/down-set.
    """
    if n <= 0:
        return set()

    target = {(0, 0)}
    frontier = {(1, 0), (0, 1)}

    while len(target) < n:
        candidates = []

        for cell in frontier:
            x, y = cell

            if x < 0 or y < 0:
                continue

            # Một ô được thêm nếu các predecessor cần thiết đã có.
            ok = True

            if x > 0 and (x - 1, y) not in target:
                ok = False

            if y > 0 and (x, y - 1) not in target:
                ok = False

            if ok:
                heapq.heappush(candidates, (cell_potential_2d(cell), x + y, x, y, cell))

        if not candidates:
            break

        _, _, _, _, chosen = heapq.heappop(candidates)
        target.add(chosen)
        frontier.discard(chosen)

        x, y = chosen
        frontier.add((x + 1, y))
        frontier.add((x, y + 1))

    return target


def manhattan(a, b):
    return abs(a[0] - b[0]) + abs(a[1] - b[1])


def target_distance_2d(blocks_set, target):
    """
    Khoảng cách xấp xỉ giữa cấu hình hiện tại và target.

    Dùng khoảng cách Manhattan hai chiều:
    - mỗi block hiện tại đến target gần nhất
    - mỗi ô target đến block hiện tại gần nhất
    """
    if not blocks_set or not target:
        return 0

    blocks = list(blocks_set)
    target_cells = list(target)

    d1 = 0
    for b in blocks:
        d1 += min(manhattan(b, t) for t in target_cells)

    d2 = 0
    for t in target_cells:
        d2 += min(manhattan(t, b) for b in blocks)

    return d1 + d2


def compaction_score_2d(blocks_set):
    """
    Score dùng để tránh kẹt local optimum.

    Khác với potential, score phạt mạnh:
    - các lỗ khiến cấu hình chưa finished
    - khoảng cách tới target compact

    Score nhỏ hơn nghĩa là cấu hình tốt hơn.
    """
    pot = potential(blocks_set, 2)
    holes = finished_holes_2d(blocks_set)
    target = compact_target_2d(len(blocks_set))
    dist = target_distance_2d(blocks_set, target)

    return pot + 25 * len(holes) + 3 * dist


def is_better_state(before, after):
    """
    Kiểm tra after có tốt hơn before không.

    Ưu tiên:
    1. finished
    2. score giảm
    3. potential giảm
    """
    if is_finished_2d(after):
        return True

    before_score = compaction_score_2d(before)
    after_score = compaction_score_2d(after)

    if after_score < before_score:
        return True

    if potential(after, 2) < potential(before, 2):
        return True

    return False


# ============================================================
# 3. Các operation paper-inspired dạng một bước
# ============================================================

def one_step_candidates(blocks_set, operation_name, filter_func=None):
    """
    Sinh candidate move cho một operation.

    Khác bản cũ:
    - Không chỉ xét potential giảm.
    - Cho phép move nếu score tổng thể tốt hơn.
    """
    result = []

    for c, e, move_type in generate_valid_moves_2d(blocks_set):
        if filter_func is not None and not filter_func(c, e, move_type):
            continue

        next_blocks = apply_move(blocks_set, c, e)

        if is_better_state(blocks_set, next_blocks):
            result.append((c, e, move_type, operation_name, next_blocks))

    return result


def choose_best_candidate(blocks_set, candidates):
    if not candidates:
        return None

    best = None
    best_key = None

    for c, e, move_type, operation, next_blocks in candidates:
        key = (
            0 if is_finished_2d(next_blocks) else 1,
            compaction_score_2d(next_blocks),
            potential(next_blocks, 2),
            e[1],
            e[0]
        )

        if best is None or key < best_key:
            best = (c, e, move_type, operation, next_blocks, potential(next_blocks, 2))
            best_key = key

    return best


def local_y_reduction(blocks_set):
    """
    Bước 1: Local y-reduction.
    Ưu tiên kéo ô xuống dưới.
    """
    def filt(c, e, move_type):
        return e[1] < c[1]

    candidates = one_step_candidates(
        blocks_set,
        "local_y_reduction",
        filt
    )

    return choose_best_candidate(blocks_set, candidates)


def is_in_nontrivial_column(c, blocks_set):
    """
    Kiểm tra c có thuộc cột dọc độ dài >= 2 không.
    """
    x, _ = c
    return sum(1 for bx, by in blocks_set if bx == x) >= 2


def column_shove(blocks_set):
    """
    Bước 2: Column shove bản heuristic.

    Ưu tiên move từ một cột dọc, hướng xuống hoặc sang trái.
    """
    def filt(c, e, move_type):
        if not is_in_nontrivial_column(c, blocks_set):
            return False

        return e[1] <= c[1] or e[0] < c[0]

    candidates = one_step_candidates(
        blocks_set,
        "column_shove",
        filt
    )

    return choose_best_candidate(blocks_set, candidates)


def local_potential_reduction(blocks_set):
    """
    Bước 3: Local potential reduction.

    Tìm bất kỳ move một bước nào làm tốt score hoặc potential.
    """
    candidates = one_step_candidates(
        blocks_set,
        "local_potential_reduction"
    )

    return choose_best_candidate(blocks_set, candidates)


# ============================================================
# 4. Low / high components trong 2D
# ============================================================

def connected_components_2d(cells):
    cells = set(cells)

    if not cells:
        return []

    components = []
    unvisited = set(cells)

    while unvisited:
        start = next(iter(unvisited))
        comp = {start}
        queue = deque([start])
        unvisited.remove(start)

        while queue:
            x, y = queue.popleft()

            for nb in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]:
                if nb in unvisited:
                    unvisited.remove(nb)
                    comp.add(nb)
                    queue.append(nb)

        components.append(comp)

    return components


def get_low_high_components_2d(blocks_set):
    low_cells = {b for b in blocks_set if b[1] == 0}
    high_cells = {b for b in blocks_set if b[1] > 0}

    return connected_components_2d(low_cells), connected_components_2d(high_cells)


def get_root_low_component(low_components):
    if not low_components:
        return None

    for comp in low_components:
        if (0, 0) in comp:
            return comp

    return low_components[0]


def non_root_low_components(blocks_set):
    low_components, _ = get_low_high_components_2d(blocks_set)
    root = get_root_low_component(low_components)

    if root is None:
        return []

    return [comp for comp in low_components if comp != root]


def is_small_low_component(component):
    """
    Heuristic 2D:
    component nhỏ nếu số ô nhỏ hơn khoảng cách từ ô trái nhất về gốc.
    """
    if not component:
        return False

    min_x = min(x for x, y in component)
    return len(component) < min_x + 1


def handling_low_components(blocks_set):
    """
    Bước 4-5: Handling low components.
    """
    comps = non_root_low_components(blocks_set)

    if not comps:
        return None

    cells = set()
    for comp in comps:
        cells.update(comp)

    def filt(c, e, move_type):
        if c not in cells:
            return False

        return e[0] < c[0] or e[1] == 0

    candidates = one_step_candidates(
        blocks_set,
        "handling_low_components",
        filt
    )

    return choose_best_candidate(blocks_set, candidates)


def small_low_components(blocks_set):
    """
    Bước 6: Small low components.
    """
    comps = non_root_low_components(blocks_set)
    small_cells = set()

    for comp in comps:
        if is_small_low_component(comp):
            small_cells.update(comp)

    if not small_cells:
        return None

    def filt(c, e, move_type):
        return c in small_cells and (e[0] <= c[0] or e[1] <= c[1])

    candidates = one_step_candidates(
        blocks_set,
        "small_low_components",
        filt
    )

    return choose_best_candidate(blocks_set, candidates)


def big_low_components(blocks_set):
    """
    Bước 7: Big low components.
    """
    comps = non_root_low_components(blocks_set)
    big_cells = set()

    for comp in comps:
        if not is_small_low_component(comp):
            big_cells.update(comp)

    if not big_cells:
        return None

    def filt(c, e, move_type):
        return c in big_cells and e[0] < c[0]

    candidates = one_step_candidates(
        blocks_set,
        "big_low_components",
        filt
    )

    return choose_best_candidate(blocks_set, candidates)


# ============================================================
# 5. Macro escape / Beam search
# ============================================================

def move_sort_key(blocks_set, move):
    """
    Sắp xếp move để beam search ưu tiên move có vẻ tốt.
    """
    c, e, move_type = move
    next_blocks = apply_move(blocks_set, c, e)

    return (
        0 if is_finished_2d(next_blocks) else 1,
        compaction_score_2d(next_blocks),
        potential(next_blocks, 2),
        e[1],
        e[0]
    )


def beam_escape_sequence_2d(
    blocks_set,
    max_depth=5,
    beam_width=80,
    branch_limit=35
):
    """
    Tìm một chuỗi move ngắn để thoát kẹt.

    Khác greedy:
    - Cho phép potential tăng tạm thời.
    - Chỉ cần sau vài bước score tốt hơn hoặc finished.
    """
    start_score = compaction_score_2d(blocks_set)
    start_pot = potential(blocks_set, 2)
    start_state = tuple(sorted(blocks_set))

    beam = [
        {
            "blocks": set(blocks_set),
            "sequence": [],
            "score": start_score,
            "potential": start_pot
        }
    ]

    visited = {start_state}

    best_sequence = None
    best_key = None

    for depth in range(1, max_depth + 1):
        next_beam = []

        for node in beam:
            current_blocks = node["blocks"]
            moves = generate_valid_moves_2d(current_blocks)

            moves = sorted(
                moves,
                key=lambda m: move_sort_key(current_blocks, m)
            )[:branch_limit]

            for c, e, move_type in moves:
                after = apply_move(current_blocks, c, e)
                state = tuple(sorted(after))

                if state in visited:
                    continue

                visited.add(state)

                seq = node["sequence"] + [(c, e, move_type, "macro_escape")]
                after_score = compaction_score_2d(after)
                after_pot = potential(after, 2)

                key = (
                    0 if is_finished_2d(after) else 1,
                    after_score,
                    after_pot,
                    len(seq)
                )

                # Điều kiện nhận chuỗi:
                # finished hoặc score giảm rõ ràng hoặc potential giảm rõ ràng sau vài bước
                accepted = (
                    is_finished_2d(after)
                    or after_score < start_score
                    or (depth >= 2 and after_pot < start_pot)
                )

                if accepted:
                    if best_sequence is None or key < best_key:
                        best_sequence = seq
                        best_key = key

                next_beam.append(
                    {
                        "blocks": after,
                        "sequence": seq,
                        "score": after_score,
                        "potential": after_pot
                    }
                )

        if best_sequence is not None:
            return best_sequence

        next_beam.sort(
            key=lambda node: (
                node["score"],
                node["potential"],
                len(node["sequence"])
            )
        )

        beam = next_beam[:beam_width]

        if not beam:
            break

    return None


# ============================================================
# 6. Chọn operation theo pipeline
# ============================================================

def choose_one_step_paper_move_2d(blocks_set):
    """
    Thử lần lượt các operation một bước.
    """
    operations = [
        local_y_reduction,
        column_shove,
        local_potential_reduction,
        handling_low_components,
        small_low_components,
        big_low_components
    ]

    for op in operations:
        move = op(blocks_set)
        if move is not None:
            return move

    return None


def choose_next_sequence_2d(blocks_set):
    """
    Trả về một sequence move.

    Thường sequence chỉ có 1 move.
    Nếu greedy bị kẹt, dùng macro_escape để tìm chuỗi nhiều bước.
    """
    one_step = choose_one_step_paper_move_2d(blocks_set)

    if one_step is not None:
        c, e, move_type, operation, next_blocks, next_pot = one_step
        return [(c, e, move_type, operation)]

    macro_seq = beam_escape_sequence_2d(
        blocks_set,
        max_depth=5,
        beam_width=80,
        branch_limit=35
    )

    if macro_seq:
        return macro_seq

    return None


# ============================================================
# 7. Vòng lặp chính
# ============================================================

def compact_2d_1(blocks, max_steps):
    """
    Thuật toán 2D paper-inspired cải tiến.

    Giữ nguyên input/output.
    Bên trong dùng:
    - guided operation one-step
    - macro_escape beam search để tránh local optimum
    """
    blocks_set = to_tuple_set(blocks)
    curr_blocks = blocks_set
    curr_pot = potential(curr_blocks, 2)

    initial_info = {
        "blocks": to_list(curr_blocks),
        "potential": curr_pot,
        "is_connected": True,
        "is_finished": is_finished_2d(curr_blocks)
    }

    steps = []
    status = "completed" if is_finished_2d(curr_blocks) else "running"

    if status == "completed":
        final_info = {
            "blocks": to_list(curr_blocks),
            "potential": curr_pot,
            "is_connected": True,
            "is_finished": True
        }

        return build_response(
            True,
            "Compaction completed successfully.",
            2,
            initial_info,
            steps,
            final_info,
            status,
            algorithm="paper_inspired_2d"
        )

    step_idx = 1

    while step_idx <= max_steps:
        if is_finished_2d(curr_blocks):
            status = "completed"
            break

        sequence = choose_next_sequence_2d(curr_blocks)

        if sequence is None:
            status = "no_valid_move_found"
            break

        for c, e, move_type, operation in sequence:
            if step_idx > max_steps:
                status = "max_steps_reached"
                break

            # Có thể sau khi chạy vài bước trong sequence, move tiếp theo không còn hợp lệ.
            # Khi đó thoát sequence và chọn lại ở vòng ngoài.
            if c not in curr_blocks or e in curr_blocks:
                break

            current_move_type = get_move_type_2d(c, e, curr_blocks)

            if current_move_type is None:
                break

            if not is_move_connected(curr_blocks, c, e, 2):
                break

            blocks_before = set(curr_blocks)
            pot_before = curr_pot

            next_blocks = apply_move(curr_blocks, c, e)
            next_pot = potential(next_blocks, 2)

            finished_after = is_finished_2d(next_blocks)
            connected_after = is_connected(next_blocks, 2)

            step_info = make_step_info(
                step_idx=step_idx,
                from_c=c,
                to_c=e,
                move_type=current_move_type,
                blocks_before=blocks_before,
                blocks_after=next_blocks,
                pot_before=pot_before,
                pot_after=next_pot,
                connected_after=connected_after,
                finished_after=finished_after,
                operation=operation
            )

            steps.append(step_info)

            curr_blocks = next_blocks
            curr_pot = next_pot
            step_idx += 1

            if finished_after:
                status = "completed"
                break

        if status in ["completed", "max_steps_reached"]:
            break

    if status == "running":
        status = "max_steps_reached" if step_idx > max_steps else "no_valid_move_found"

    final_finished = is_finished_2d(curr_blocks)

    if final_finished:
        status = "completed"

    final_info = {
        "blocks": to_list(curr_blocks),
        "potential": curr_pot,
        "is_connected": is_connected(curr_blocks, 2),
        "is_finished": final_finished
    }

    if status == "completed":
        message = "Compaction completed successfully."
    elif status == "max_steps_reached":
        message = "Maximum number of steps reached before finishing compaction."
    else:
        message = "No valid move found."

    return build_response(
        True,
        message,
        2,
        initial_info,
        steps,
        final_info,
        status,
        algorithm="paper_inspired_2d"
    )