规矩出方圆

思路

在单位正方形上有一个内切圆(圆心 ,半径 )。我们放 条水平和竖直切割线(含边界),将正方形切成若干小矩形。只要一个矩形和圆的交集面积 ,就算"被染色"。目标是让被染色的小矩形总面积最小。

先想几个关键问题:

什么样的矩形会被"浪费"? 被染色的矩形分两类——完全在圆内的(不可避免,面积贡献 = 圆面积),和横跨圆边界的"边界格子"。边界格子的整块面积被计入,但实际只有一部分落在圆内。我们的目标就是让这些边界格子尽可能薄。

怎么利用对称性? 圆关于中心 有四重对称性。最优方案可以让横、纵切割线用同一组坐标,并且关于 对称。

核心转化——角度参数化。,对称切割点位于 ,其中 中的角度。这等价于把圆弧上的点投影到坐标轴。每条切割线正好穿过圆上某个角度对应的点。

最优的自洽条件。 当切割线位于 时,圆在 处的两个交点的 坐标分别是 。如果令 (角度关于 对称),那么 ,圆的交点恰好落在另一条切割线上!这意味着边界格子在角落处恰好"贴着"圆弧。

在这个对称约束下,一个格子 位于圆外(不被染色)当且仅当 。这使得"未着色面积"的表达式变得非常干净:

$$

其中 是第 条带的宽度, 是前 条带宽度之和。

解析梯度 + L-BFGS。 由于角度对称,自由变量只有 个( 为奇数时中间角度固定为 )。对每个自由角度 ,梯度可以解析推导:

$$

用 L-BFGS 算法几十次迭代就能收敛到最优解,对 也只需不到 0.1 秒。

代码

import math

def solve():
    M = int(input())
    k = (M - 1) // 2
    n_free = k // 2
    k_odd = k % 2
    pi4 = math.pi / 4
    cos_pi4 = 0.7071067811865476

    if n_free == 0:
        if k == 0:
            print("1.0000")
        elif k == 1:
            w = 0.5 * (1.0 - cos_pi4)
            print(f"{1.0 - 4.0 * w * w:.4f}")
        elif k == 2:
            pass
        else:
            print("1.0000")
        if k <= 1:
            return

    n = n_free if n_free > 0 else 1
    if k == 2:
        n = 1

    def eval_grad(params):
        sb = sorted(params)
        cos_b = [math.cos(b) for b in sb]
        sin_b = [math.sin(b) for b in sb]

        cos_a = list(cos_b)
        if k_odd:
            cos_a.append(cos_pi4)
        for i in range(n - 1, -1, -1):
            cos_a.append(sin_b[i])

        prev = 1.0
        prefix = [0.0] * (k + 1)
        widths = [0.0] * k
        for i in range(k):
            widths[i] = 0.5 * (prev - cos_a[i])
            prefix[i + 1] = prefix[i] + widths[i]
            prev = cos_a[i]

        uc = 0.0
        prev = 1.0
        for i in range(k):
            uc += widths[i] * prefix[k - i]
        val = 1.0 - 4.0 * uc

        sort_order = sorted(range(n), key=lambda i: params[i])
        grad_sorted = [0.0] * n
        for j in range(n):
            grad_sorted[j] = 4.0 * (widths[j] * cos_b[j] - widths[k-1-j] * sin_b[j])

        grad = [0.0] * n
        for i, si in enumerate(sort_order):
            grad[si] = grad_sorted[i]

        return val, grad

    params = [(i + 1) * pi4 / (n + 1) for i in range(n)]

    lo_bound = 1e-10
    hi_bound = pi4 - 1e-10

    m_lbfgs = min(10, n)
    s_list = []
    y_list = []
    rho_list = []

    val, grad = eval_grad(params)

    for iteration in range(5000):
        if math.sqrt(sum(g*g for g in grad)) < 1e-13:
            break

        q = list(grad)
        alphas_bfgs = []
        for i in range(len(s_list) - 1, -1, -1):
            alpha = rho_list[i] * sum(s_list[i][j]*q[j] for j in range(n))
            alphas_bfgs.append(alpha)
            for j in range(n):
                q[j] -= alpha * y_list[i][j]
        alphas_bfgs.reverse()

        if s_list:
            ys = sum(y_list[-1][j]*s_list[-1][j] for j in range(n))
            yy = sum(y_list[-1][j]**2 for j in range(n))
            gamma = ys / yy if yy > 0 else 1.0
        else:
            gamma = 0.01

        r = [gamma * qi for qi in q]

        for i in range(len(s_list)):
            beta = rho_list[i] * sum(y_list[i][j]*r[j] for j in range(n))
            for j in range(n):
                r[j] += s_list[i][j] * (alphas_bfgs[i] - beta)

        direction = [-ri for ri in r]

        step = 1.0
        old_val = val
        for ls in range(30):
            new_params = [max(lo_bound, min(hi_bound, params[i] + step * direction[i])) for i in range(n)]
            new_val, new_grad = eval_grad(new_params)
            if new_val < old_val - 1e-15 * abs(old_val):
                break
            step *= 0.5
        else:
            break

        s = [new_params[j] - params[j] for j in range(n)]
        y = [new_grad[j] - grad[j] for j in range(n)]
        sy = sum(s[j]*y[j] for j in range(n))

        if sy > 1e-15:
            if len(s_list) >= m_lbfgs:
                s_list.pop(0)
                y_list.pop(0)
                rho_list.pop(0)
            s_list.append(s)
            y_list.append(y)
            rho_list.append(1.0 / sy)

        params = new_params
        val = new_val
        grad = new_grad

    print(f"{val:.4f}")

solve()

复杂度分析

  • 时间复杂度:每次 L-BFGS 迭代需要 计算函数值和梯度,总迭代次数约为 级别,故总复杂度约 ,其中
  • 空间复杂度,存储角度参数、宽度数组和 L-BFGS 历史向量。