利用Duckdb求解Advent of Code 2025第12题 摆放礼物

AI赋能编程语言挑战赛 10w+人浏览 228人参与

原题地址

今年的Advent of Code 日程缩短了,这是最后一题。

— 第 12 天:圣诞树农场 —
你的时间不多了,但应该没剩下多少需要装饰的东西了。虽然这里没有楼梯、电梯、自动扶梯、隧道、滑道、传送器、消防杆或管道能带你深入北极基地,但有一个通风管道。你跳了进去。

颠簸了几分钟后,你出现在一个大型、光线充足的洞穴里,里面全是圣诞树!

有几只精灵在这里疯狂地装饰,赶在截止日期前完成任务。他们认为自己能够完成大部分工作,但唯一担心的是给所有住在北极的小精灵的礼物。把礼物放在树下面是一个古老的传统,但精灵们担心礼物放不下。

礼物有几种标准但非常奇怪的形状。形状和它们需要放入的区域都以标准单位测量。为了美观,礼物需要按照标准化的二维单位网格放入区域中;你也不能堆叠礼物。

一如既往,精灵们为你准备了情况摘要(你的谜题输入)。首先,它包含礼物形状的列表。其次,它包含每棵树下区域的大小以及需要放入该区域的每种形状礼物的数量列表。例如:

0:
###
##.
##.

1:
###
##.
.##

2:
.##
###
##.

3:
##.
###
##.

4:
###
#..
###

5:
###
.#.
###

4x4: 0 0 0 0 2 0
12x5: 1 0 1 0 2 2
12x5: 1 0 1 0 3 2

第一部分列出了标准礼物形状。为了方便,每个形状以索引和冒号开头;然后形状以可视方式显示,其中 # 表示形状的一部分,. 表示不是。

第二部分列出了树下的区域。每行以区域的宽度和长度开头;12x5 表示区域宽 12 单位,长 5 单位。该行的其余部分通过列出每种形状礼物的数量来描述需要放入该区域的礼物;1 0 1 0 3 2 意味着你需要放入一个形状索引为 0 的礼物,零个形状索引为 1 的礼物,一个形状索引为 2 的礼物,零个形状索引为 3 的礼物,三个形状索引为 4 的礼物和两个形状索引为 5 的礼物。

礼物可以旋转和翻转以便放入可用空间,但它们必须始终完美地放在网格上。形状不能重叠(即,两个不同礼物的 # 部分不能放在网格的同一位置),但它们可以拼合在一起(即,礼物形状图中的 . 部分不会阻止另一个礼物占据网格上的那个空间)。

精灵们需要知道有多少区域可以容纳列出的所有礼物。在上面的例子中,有六个独特的礼物形状和三个需要检查的区域。

第一个区域是 4x4

....
....
....
....

在这个区域中,你需要确定是否可以放入两个形状索引为 4 的礼物:

###
#..
###

经过一些尝试,结果是可以将两个礼物都放入这个区域。这是一种方法,用 A 代表一个礼物,B 代表另一个:

AAA.
ABAB
ABAB
.BBB

第二个区域,12x5: 1 0 1 0 2 2,宽 12 单位,长 5 单位。在这个区域中,你需要尝试放入一个形状索引为 0 的礼物,一个形状索引为 2 的礼物,两个形状索引为 4 的礼物和两个形状索引为 5 的礼物。

结果证明所有这些礼物都可以放入这个区域。这是一种方法,同样使用不同的大写字母代表所有需要的礼物:

....AAAFFE.E
.BBBAAFFFEEE
DDDBAAFFCECE
DBBB....CCC.
DDD.....C.C.

第三个区域,12x5: 1 0 1 0 3 2,与上一个区域大小相同;唯一的区别是这个区域需要多放入一个形状索引为 4 的礼物。不幸的是,无论怎么尝试,都无法将所有礼物放入这个区域。

所以,在这个例子中,2 个区域可以容纳所有列出的礼物。

考虑每棵树下方的区域以及精灵们希望放入每个区域的礼物。有多少个区域可以容纳所有列出的礼物?

解题分析
这看上去像一个扩展的俄罗斯方块问题,但不是标准的形状,有些形状之间无法紧密咬合,会留下空洞。让DeepSeek按如下思路

把标准形状换算成二进制数,算出它旋转和翻转后的二进制数,每次加入区域时,新放入的要和区域已有的二进制数bitand为0,放好后就两者相加作为区域新的二进制数,用这种思路编写python程序

结果他写出来了

from itertools import permutations, combinations
import re

class Shape:
    def __init__(self, index, pattern):
        self.index = index
        self.pattern = pattern
        self.height = len(pattern)
        self.width = len(pattern[0])
        
        # 生成所有旋转和翻转的二进制表示
        self.all_variants = self.generate_all_variants()
    
    def pattern_to_binary(self, pattern):
        """将图案转换为二进制数"""
        binary = 0
        for y in range(len(pattern)):
            for x in range(len(pattern[0])):
                if pattern[y][x] == '#':
                    bit_pos = y * self.width + x
                    binary |= (1 << bit_pos)
        return binary
    
    def rotate_90(self, pattern):
        """顺时针旋转90度"""
        h, w = len(pattern), len(pattern[0])
        rotated = [['.' for _ in range(h)] for _ in range(w)]
        for y in range(h):
            for x in range(w):
                rotated[x][h-1-y] = pattern[y][x]
        return rotated
    
    def flip_horizontal(self, pattern):
        """水平翻转"""
        h, w = len(pattern), len(pattern[0])
        flipped = [['.' for _ in range(w)] for _ in range(h)]
        for y in range(h):
            for x in range(w):
                flipped[y][w-1-x] = pattern[y][x]
        return flipped
    
    def generate_all_variants(self):
        """生成所有可能的旋转和翻转变体"""
        variants = set()
        
        # 原始图案
        patterns = [self.pattern]
        
        # 生成所有旋转
        for _ in range(3):
            patterns.append(self.rotate_90(patterns[-1]))
        
        # 为每个旋转生成翻转版本
        all_patterns = []
        for p in patterns:
            all_patterns.append(p)
            all_patterns.append(self.flip_horizontal(p))
        
        # 转换为二进制并去重
        for pattern in all_patterns:
            binary = self.pattern_to_binary(pattern)
            h = len(pattern)
            w = len(pattern[0])
            variants.add((binary, h, w))
        
        return list(variants)

def parse_input(filename):
    """解析输入文件"""
    with open(filename, 'r') as f:
        lines = f.read().strip().splitlines()
    
    shapes = []
    regions = []
    
    i = 0
    # 解析形状
    while i < len(lines):
        line = lines[i].strip()
        if not line:
            i += 1
            continue
            
        if line.endswith(':'):
            # 新形状
            index = int(line[:-1])
            pattern = []
            i += 1
            while i < len(lines) and lines[i].strip() and 'x' not in lines[i]:
                pattern.append(list(lines[i].strip()))
                i += 1
            shapes.append(Shape(index, pattern))
        elif 'x' in line:
            # 区域
            parts = line.split(':')
            size_part = parts[0].strip()
            width, height = map(int, size_part.split('x'))
            quantities = list(map(int, parts[1].strip().split()))
            regions.append((width, height, quantities))
            i += 1
        else:
            i += 1
    
    return shapes, regions

def can_fit_in_region(shapes, region_info, shape_counts, idx=0, region_state=0):
    """递归检查所有形状是否能放入区域"""
    if idx == len(shape_counts):
        return True
    
    shape_idx, count = shape_counts[idx]
    shape = shapes[shape_idx]
    
    region_height=region_info[1]
    region_width=region_info[0]
    # 尝试放入指定数量的当前形状
    for _ in range(count):
        placed = False
        
        # 尝试当前形状的所有变体
        for binary, h, w in shape.all_variants:
            # 检查形状尺寸是否适合区域
            if h > region_height or w > region_width:
                continue
            
            # 尝试所有可能的位置
            for y in range(region_height - h + 1):
                for x in range(region_width - w + 1):
                    # 将形状二进制数移动到正确位置
                    shifted_binary = 0
                    for dy in range(h):
                        for dx in range(w):
                            if (binary >> (dy * w + dx)) & 1:
                                new_pos = (y + dy) * region_width + (x + dx)
                                shifted_binary |= (1 << new_pos)
                    
                    # 检查是否与已有形状冲突
                    if (region_state & shifted_binary) == 0:
                        # 放置形状并递归
                        if can_fit_in_region(shapes, region_info, shape_counts, idx, region_state | shifted_binary):
                            placed = True
                            region_state |= shifted_binary
                            break
                
                if placed:
                    break
            
            if placed:
                break
        
        if not placed:
            return False
        
        # 如果还有更多相同形状,继续尝试放置
        # 这里简化处理:尝试放完所有数量
    
    # 继续处理下一个形状
    return can_fit_in_region(shapes, region_info, shape_counts, idx + 1, region_state)

def solve():
    shapes, regions = parse_input('2512-input0.txt')
    
    count_fit = 0
    region_num = 1
    
    for width, height, quantities in regions:
        region_width = width
        region_height = height
        
        # 创建形状计数列表,过滤掉数量为0的形状
        shape_counts = [(i, quantities[i]) for i in range(len(quantities)) if quantities[i] > 0]
        
        # 计算总单元数检查
        total_cells = sum(shape_counts[i][1] * sum(row.count('#') for row in shapes[shape_counts[i][0]].pattern) 
                         for i in range(len(shape_counts)))
        
        if total_cells > width * height:
            # 基本检查:如果所有形状的单元格总数超过区域面积,肯定放不下
            print(f"区域 {region_num}: {width}x{height} - 总面积不足,无法放入")
        else:
            # 尝试放置
            if can_fit_in_region(shapes, (width, height, quantities), shape_counts, 0, 0):
                print(f"区域 {region_num}: {width}x{height} - 可以放入")
                count_fit += 1
            else:
                print(f"区域 {region_num}: {width}x{height} - 无法放入")
        
        region_num += 1
    
    print(f"\n可以容纳所有礼物的区域数量: {count_fit}")
    return count_fit

if __name__ == "__main__":
    solve()

上述程序能处理示例数据,但非常慢,主要是搜索空间太大,有旋转和翻转,又增加了分支数量,一开始空余太多,可随便乱放,到后面本来放下的也放不下了,这些都是无效搜索。所以需要合适的启发式算法剪枝。以俄罗斯方块的经验,要尽量填满一行才能消除。先把他们撘成可以互相咬合的大块比较好。

从问题规模看,正式输入数据1000个问题,6个礼物占的方块都是3x3,要求基本上都47x49: 60 48 64 49 52 78 这种级别,需要更高级的算法。从数独问题联想到完全覆盖问题。

俄罗斯方块覆盖问题一共有多少个解

用舞蹈链算法(Dancing Links)求解俄罗斯方块覆盖问题

上面文章用来覆盖的方块才8x8大小,而aoc问题允许空洞,不是完全覆盖,不知能不能用单个1x1方块填上,然后根据总面积-要求的方块=1方块的个数。这样就转成完全覆盖问题了。

让他改Dancing-Links X算法,没成功。让AI编程就存在一个问题,如果自己不熟悉算法,他的程序对了也糊涂,错了也不会改。

张泽鹏先生没有直接编写,而是先对数据分析,结果有很多尺寸是明显不足的,可以直接过滤掉 488 个,我又问他,有没有特别宽松的,直接按3x3摆放就行的?结果出乎意料。

他说:“笑死了……题目搞得这么复杂,居然是最简单的逻辑就通过了”。

我还没明白是什么逻辑,他又提示“结果和礼物的形状无关”,我也用输入数据填充到数据库表一试,这个数据揭开了真相:

memory D create table t as (from read_csv('2512data.txt',header=0,delim=' ')t(a,b,c,d,e,f,g,h));
memory D select count(*) from t where a*b<(c+d+e+f+g+h)*7-(d+g);
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│     515      │
└──────────────┘
memory D select count(*) from t where a//3*3*b//3*3>=(c+d+e+f+g+h)*9;
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│     485      │
└──────────────┘
memory D select count(*) from t;
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│     1000     │
└──────────────┘

表中一共就两类数据,一类绝对不可能放得下,礼物小方块总面积数比待覆盖长方形面积还多;一类绝对可以放得下的,就是前面提到的特别宽松的。我的数据集里加起来正好是1000个,所以根本不用其他多余判断,绝对能放得下的就是答案。

这个问题就这样结束了。那些辛苦写出正确程序的人会不会骂人呢,张先生说:“应该不会,大家都是会先分析一下数据,提前剪枝。”

其实我不该那么早让deepseek去编,欲速不达,有了想法没有付诸行动,导致错失了先解出的机会。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值