Day 114-Q4 — AND畳み込み(Superset Zeta/Möbius変換によるSubset Convolution AND版)

2026-08-06 赤色 Master / Phase 8+ ★★★★★★★★★ 高速ゼータ変換(上位集合和)・ビット演算畳み込み

問題

$2^n$ 個の整数からなる数列 $f_0, f_1, \dots, f_{2^n-1}$ と $g_0, g_1, \dots, g_{2^n-1}$ が与えられる(添字はビット列とみなす)。

次の AND畳み込み を計算せよ。 $$ h_x = \sum_{i \,\&\, j = x} f_i \cdot g_j \pmod{998244353} $$ $h_0, h_1, \dots, h_{2^n-1}$ を空白区切りで出力せよ。

入力形式

n
f_0 f_1 ... f_{2^n-1}
g_0 g_1 ... g_{2^n-1}

制約

$1 \le n \le 20$
$0 \le f_i, g_i < 998244353$

入出力例

入力例1

2
1 2 3 4
5 6 7 8

出力例1

103 52 73 32

例えば$h_3$($x=11_2$)は$i\&j=3$となる$(i,j)$、つまり$i=j=3$の1通りのみで$f_3g_3=4\times8=32$。$h_2$($x=10_2$)は$i\&j=2$となる組$(2,2),(2,3),(3,2)$の3通りで$21+24+28=73$。全体の和は$\sum h_x=(\sum f_i)(\sum g_j)=10\times26=260$で検算できる。

概念図: 上位集合和(Superset Sum)でANDを畳み込む

OR畳み込み(部分集合和)とAND畳み込み(上位集合和)は互いに双対 OR畳み込み: SOS(部分集合和) f̂[x] = Σ_{y⊆x} f[y] 更新: 立っているビット側に 立っていない側の値を加算 AND畳み込み: 上位集合和 f̂[x] = Σ_{y⊇x} f[y] 更新: 立っていないビット側に 立っている側の値を加算(逆向き) ゼータ変換 → 点ごとの積 → メビウス変換(逆変換)で h[x] = Σ_{i&j=x} f_i g_j が得られる 各ビットについてO(2^n)の更新をn回繰り返すのでO(2^n・n)

ヒント(段階的開示)

ヒント1: 方向性
OR畳み込み($i\mid j=x$)は「部分集合和」の高速ゼータ変換(SOS DP)で解けることが知られている。AND畳み込みはその双対で、「上位集合和(superset sum)」の高速ゼータ変換を使う。
ヒント2: アプローチ
$\zeta^{\supseteq}(f)[x] = \sum_{y \supseteq x} f_y$ を定義する。各ビット $b$ について、$x$ のビット $b$ が $0$ のとき $f[x] \mathrel{+}= f[x | (1<
ヒント3: 誘導(コード骨格)
def superset_zeta(a, n):
    for b in range(n):
        for x in range(1 << n):
            if not (x >> b) & 1:
                a[x] = (a[x] + a[x | (1 << b)]) % MOD

def superset_mobius(a, n):
    for b in range(n):
        for x in range(1 << n):
            if not (x >> b) & 1:
                a[x] = (a[x] - a[x | (1 << b)]) % MOD

# F = superset_zeta(f); G = superset_zeta(g)
# H[x] = F[x] * G[x]  (pointwise)
# h = superset_mobius(H)

模範解答 (Python)

import sys

MOD = 998244353

def main():
    data = sys.stdin.buffer.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    size = 1 << n
    f = [int(data[idx + i]) % MOD for i in range(size)]; idx += size
    g = [int(data[idx + i]) % MOD for i in range(size)]; idx += size

    def superset_zeta(a):
        for b in range(n):
            bit = 1 << b
            for x in range(size):
                if not (x & bit):
                    a[x] = (a[x] + a[x | bit]) % MOD

    def superset_mobius(a):
        for b in range(n):
            bit = 1 << b
            for x in range(size):
                if not (x & bit):
                    a[x] = (a[x] - a[x | bit]) % MOD

    F = f[:]
    G = g[:]
    superset_zeta(F)
    superset_zeta(G)

    H = [(F[x] * G[x]) % MOD for x in range(size)]

    superset_mobius(H)

    print(" ".join(str(v % MOD) for v in H))

main()
計算量: ゼータ変換・メビウス変換ともに $O(2^n \cdot n)$。$n=20$ で $2^{20}\times20 \approx 2\times10^7$ 回の演算を3回。ブルートフォース $O(4^n)$($n\le10$程度)との一致を確認済み(例1のブルートフォース値と模範解答の出力103 52 73 32が完全一致)。

Step-by-Step 解説

1OR畳み込みとの対比で理解する
OR畳み込みは「部分集合和」$\hat f[x]=\sum_{y\subseteq x}f_y$を使う。AND畳み込みはこれの双対で「上位集合和」を使い、更新の向きが逆になる。
2上位集合和ゼータ変換の意味
$\zeta^{\supseteq}(f)[x]=\sum_{y\supseteq x}f_y$は「$x$を部分集合として含むすべての$y$」の和。各ビットについて独立に更新すれば計算できる。
3なぜ点ごとの積が畳み込みになるのか
$\zeta^{\supseteq}(f)[x]\cdot\zeta^{\supseteq}(g)[x]=\sum_{i\supseteq x}\sum_{j\supseteq x}f_ig_j$。これを$x$についてメビウス変換すると、$i\&j=x$の組だけが残る。
4メビウス変換は差分を取るだけ
ゼータ変換の逆操作は、同じループ構造で加算を減算に変えるだけで実現できる。
5MODと負の値の扱い
メビウス変換では引き算が発生し途中で負の値になりうる。Pythonの%は負数でも正の余りを返すが、他言語では+MODしてから%MODする必要がある。

よくあるミス

ミス原因正しい書き方
OR畳み込みと同じ更新方向(部分集合和)を使ってしまうSOS DPのコードをそのまま流用するAND畳み込みは「ビットが立っていない側に、立っている側の値を足す」逆方向の更新を使う
ゼータ変換前にf,gを直接書き換えて元の配列を破壊するインプレース更新の副作用を考慮しないf[:]でコピーしてから変換する
ビット$b$のループと$x$のループの順序を間違える「ビットごとに全体を1回走査する」構造を理解していない外側をビット$b$、内側を$x$の全走査にする
$n$が大きいときPythonのループが遅くて間に合わない純粋なPythonループの定数倍を過小評価するNumPyのベクトル化やarrayモジュールでの実装を検討する

次のステップ

  • 発展: XOR畳み込み(Walsh-Hadamard変換、Day059 Q1)・OR畳み込み(Day020 Q2)とAND畳み込みを統一的に理解し、3つの違いを一覧化する
  • 発展: 「AND畳み込みを$k$回繰り返した結果」を高速に求める(ゼータ変換した状態で$k$乗してから1回だけ逆変換すればよい)
  • 発展: 部分集合の要素数制約を持つ本来のSubset Convolutionとの違いを整理する

自己評価

自分の回答

気づき・メモ