「アルゴリズム大会・クイック300問」 1日1問「バイナリ数独」

アルゴリズム コンペティション: クイック 300 問」は 2024 年に出版される予定で、「アルゴリズム コンペティション」の補助問題集です。
すべての質問は、自作の OJ New Online Judgeに配置されます。
コードは C/C++、Java、Python の 3 つの言語で提供されており、トピックは主に中レベルから低レベルのトピックであり、初級レベルから上級レベルの学生に適しています。


バイナリ数独 」、リンク: http://oj.ecustacm.cn/problem.php?id=1872

質問の説明

[問題の説明]農夫のジョンは牛たちと面白い数独ゲームをしています。
従来の Sudoku と同様、このゲームも 9x9 の正方形で構成され、9 つの 3x3 の小さな正方形に分割されます。
ただし、異なるのは、このゲームでは、これらの正方形を埋めるために 2 進数の 0 と 1 のみが使用されることです。
ゲームの目的は、すべての行、列、および 3x3 の正方形に偶数の 1 が含まれるように、できるだけ少ない数値を変更することです。
たとえば、次は法的な解決策です:
000 000 000
001 000 100
001 000 100

000 110 000
000 110
000 000 000 000

000 000 000
000 000 000
000 000 000
特定の初期状況に対して、これらの牛が最小の変更数を計算するのを手伝う必要があります。
【入力形式】 1行9文字で9行入力します。
各文字は 0 または 1 です。
【出力形式】答えを表す整数を出力
【入力例】

000000000
001000100
000000000
000110000
000111000
000000000
000000000
000000000
000000000

【出力サンプル】

3

答え

   要約質問: 9×9 01 の行列が与えられた場合、各行、各列、および各 9 正方形グリッドの 1 の数が偶数になるように、少なくともいくつの数値を変更できるかを尋ねます。この例では、変更方法の 1 つは、上の 2 つの 1 と右下隅の 1 を 0 に変更し、合計 3 つ変更します。
   最小限の変更を取得するにはどうすればよいですか? まず総当たり検索を試し、DFS エンコードを使用して、考えられるすべての変更を試してください。さまざまなメソッドの変更数を比較し、最小の変更数が答えになります。可能な変更はいくつありますか? 9×9=81 個のグリッドがあり、各グリッドは 2 つの方法 (0 または 1) で変更でき、合計2 81 2^{81}281通りの変更方法。2 81 2^{81}281は明らかに大きすぎますが、枝刈りを追加すると、大幅に最適化できます。
   もう一つの種類の暴力法があります。グリッドは合計 81 個あり、変更の最小数は 1 で、最大数は 81 です。変更の数を小さいものから大きいものまで 1 つずつ判断します。まず 1 つのグリッドだけを変更して、合計 81 個の変更方法を試して、それぞれの方法で目標を達成できるかどうかを確認します。達成できない場合は、再度 2 つのグリッドを変更して、合計 81 × 80 個の変更方法を試します。読者は、この総当たり法の計算量が上記の総当たり法の計算量と同等であることを証明できます。二分法を使用して最適化することもできますが、81 を log81 に最適化するだけで、その他の計算量は依然として膨大です。
   この質問は、変更方法がいくつあるかということではなく、変更の最小数についてです。この最適性の問題を解決するには、DP の使用を検討してください。
   変更プロセスを以下でシミュレートします。左上隅の座標が (0,0)、右下隅の座標が (8,8) であるとします。各グリッドを左から右、上から下の順に変更します。左上隅 (0,0) から開始して、最初に行 0 を変更し、次に行 1 を行 8 まで変更します。
   この問題では、各行、各列、および 3×3 の正方形に偶数個の 1 が含まれる必要があるため、これら 3 つの要件に従って DP を設計します。DP の手順は次のとおりです。
   (1) 行 0 で、DP を使用して行 0 の 9 列グリッドの変更の最小数を記録します。行 0 に偶数の 1 があるかどうかを確認する必要があります。
   (2) 行 1 では、DP を使用して行 0 ~ 1 の 9 列グリッドの最小変更数を記録します。行 1 に偶数個の 1 があるかどうかを確認する必要があります。
   (3) 2 行目では、DP レコードを使用して、9 列グリッドの最小変更回数を 0 行目から 2 行目に記録します。2 行目に偶数の 1 があるかどうかを検証する必要があります。 3 つの 3×3 の 9 正方形グリッドには偶数が含まれています。1.
   8行目まで待ちます。
   それぞれの小さなグリッドには 0 または 1 が含まれており、状態を使用して DP を圧縮することは容易に考えられます。ステータス dp[][][][][]、dp[r][c][mask][sub][row] を定義する意味は次のとおりです。 (1) 現在の到着位置 (r, c)、つまり、r 行
   目 c 列です。コード内の r と c の範囲は 0 ~ 8 です。
   (2) マスクは、各列に数字 1 が出現する回数を表します。現在 r 行にいると仮定し、0 から r 行の各列にある 1 の数が偶数であるかどうかを数えます。状態圧縮の使用を簡略化するために、マスクは 9 ビットの 2 進数であり、各ビットは各列の数値 1 が出現する回数を表し、奇数は 1、偶数は 0 になります。たとえば、000000001 は、最後の列 (列 8) に奇数の 1 があり、他の列には偶数の 1 があることを意味します。
   (3) sub は、3 列ごとに数字 1 が出現する回数を表します。また、状態圧縮を使用すると、 sub は 3 ビットの 2 進数です。3 ビットはそれぞれ、列0、2、3、5、および列 6 ~ 8 に数字 1 が出現する回数を表します。奇数の回数は 1 です。 、偶数は 0 です。
   (4) row は現在の行での数値 1 の出現回数を表し、奇数回は 1、偶数回は 0 です。
   DP は dfs() プログラミングを使用して、(r, c) 位置にある小さなグリッドを行 0 と列 0 から開始して最後の行 8 と列 8 まで 1 つずつ処理します。各グリッドを変更するには 2 つの方法があり、1 または 0 に変更します。
   (1) 変更数 ans を 1 に変更します:
      ans = !a[r][c] + dfs(r, c+1,マスク ^ (1<<c), sub^(1<<(c/3) ) ), !row);
   !a[r][c]: 元の a[r][c] = 0 の場合、a[r][c] = 1 (変更の数 ans+) に変更されます。 1; 元の a[ r][c] = 1 の場合、まだ a[r][c] = 1 が存在し、変更の数は変わりません。どちらの場合も、追加の変更の数は !a[r][c] です。
   c+1: dfs を続行し、1 列右に移動します。
   マスク ^ (1<<c): 列 c に余分な 1 があります。列 c の 1 の数を新しい奇数または偶数の値に更新します。
   sub^(1<<(c/3)): 3 列ごとのパリティを更新します。たとえば、c = 2、c/3 = 0 (c が最初の 3 列にあることを意味します) の場合、パリティの数を更新します。最初の 3 列の 1。
   !row: 現在の行の 1 の数のパリティを更新します。この行にはもう 1 つ 1 があるため、新しい行は元の行の逆になります。
   (2) 変更回数 ans を 0 に変更します:
      ans = min(ans, a[r][c] + dfs(r, c + 1, Mask, sub, row));
   a[r][c] : 元々 a[r][c] = 1 だったが、現在 a[r][c] = 0 に変更された場合、変更の数 ans+1; 元々 a[r][c] = 0 の場合、変更の数は変わりません。どちらの場合も、追加の変更の数は a[r][c] です。
   c+1: dfs を続けて 1 列右に進み、
   マスク、サブ、行: グリッド (r, c) が 0 になり 1 増加しないため、すべて変更されません。
   その他の処理についてはコードを参照してください。
【ポイント】状態圧縮DP。

C++ コード

#include<bits/stdc++.h>
using namespace std;
const int INF = 999;
bool a[9][9];         //存方格矩阵,行标0~8,列标0~8
int dp[9][9][1<<9][1<<3][2];  //dp[r][c][mask][sub][row]
int dfs(int r, int c, int mask, int sub, bool row){
    
    
    if(r == 9)        //0-8行已经填满,必须保证mask=0,sub=0,row=0
        return (!mask && !sub && !row) ? 0 : INF;
    if(c == 9){
    
           //0-8列已经填完
        if(row)  return INF;                //1、保证本行偶数个
        if(r%3 == 2 && sub)  return INF;    //2、保证每三行统计一下每三列数字1出现次数为偶数个
        return dfs(r + 1, 0, mask, sub, 0); //3、下一行
    }
    int& ans = dp[r][c][mask][sub][row];    //ans是dp的别名,把下面的ans改成dp,结果一样
    if(ans != -1) return ans;  //记忆化
    ans = !a[r][c] + dfs(r, c+1, mask ^ (1<<c), sub^(1<<(c/3)), !row);
        //a[r][c]设置为1。  若原来a[r][c]=0,ans+1
    ans = min(ans, a[r][c] + dfs(r, c + 1, mask, sub, row));
        //a[r][c]设置为0。  若原来a[r][c]=1,ans+1
    return ans;
}
int main(){
    
    
    for(int i = 0; i < 9; i++){
    
    
        string s;  cin >> s;
        for(int j = 0; j < 9; j++)
            a[i][j] = (s[j] == '1'); //存到 a[0][0]~a[8][8]
    }
    memset(dp, -1, sizeof(dp));
    cout<<dfs(0, 0, 0, 0, 0)<<endl;
}

Javaコード

import java.util.*;
import java.io.*;
public class Main {
    
    
    private static final int INF = 999;
    private static boolean[][] a; // 存方格矩阵,行标0~8,列标0~8
    private static int[][][][][] dp; // dp[r][c][mask][sub][row]
    private static int dfs(int r, int c, int mask, int sub, int row) {
    
    
        if (r == 9)  // 0-8行已经填满,必须保证mask=0,sub=0,row=0
            return (mask == 0 && sub == 0 && row == 0) ? 0 : INF;        
        if (c == 9) {
    
     // 0-8列已经填完
            if (row==1)  return INF;   // 1、保证本行偶数个
            if (r % 3 == 2 && sub != 0)    return INF; 
// 2、保证每三行统计一下每三列数字1出现次数为偶数个
            return dfs(r + 1, 0, mask, sub, 0); // 3、下一行
        }
        if (dp[r][c][mask][sub][row] != -1)    // 记忆化
            return dp[r][c][mask][sub][row];
        int ans;
        ans = (!a[r][c]?1:0) + dfs(r, c+1, mask ^ (1<<c), sub^(1<<(c/3)), 1 - row);
        ans = Math.min(ans, (a[r][c]?1:0) + dfs(r, c + 1, mask, sub, row));
        dp[r][c][mask][sub][row] = ans;
        return ans;
    }
    public static void main(String[] args) {
    
    
        Scanner scanner = new Scanner(System.in);
        a = new boolean[9][9];
        for (int i = 0; i < 9; i++) {
    
    
            String s = scanner.next();
            for (int j = 0; j < 9; j++) 
                a[i][j] = (s.charAt(j) == '1'); // 存到 a[0][0]~a[8][8]            
        }
        dp = new int[9][9][1 << 9][1 << 3][2];
        for (int[][][][] rows : dp) 
            for (int[][][] row : rows) 
                for (int[][] sub : row) 
                    for (int[] arr : sub) 
                        Arrays.fill(arr, -1);
        System.out.println(dfs(0, 0, 0, 0, 0));
    }
}

Pythonコード

INF = 999
a = [[False for j in range(9)] for i in range(9)] # 存方格矩阵,行标0~8,列标0~8
dp = [[[[[-1 for k in range(2)] for j in range(1 << 3)] for i in range(1 << 9)] for c in range(9)] for r in range(9)]
def dfs(r, c, mask, sub, row):
    if r == 9: # 0-8行已经填满,必须保证mask=0,sub=0,row=0
        return 0 if not mask and not sub and not row else INF
    if c == 9: # 0-8列已经填完
        if row:   return INF       # 1、保证本行偶数个
        if r % 3 == 2 and sub: 
            return INF             # 2、保证每三行统计一下每三列数字1出现次数为偶数个
        return dfs(r + 1, 0, mask, sub, False)     # 3、下一行
    if dp[r][c][mask][sub][row] != -1:
        return dp[r][c][mask][sub][row] # 记忆化
ans = dfs(r, c+1, mask ^ (1 << c), sub ^ (1 << (c // 3)), not row) + (not a[r][c]) 
# a[r][c]设置为1。若原来a[r][c]=0,ans+1
ans = min(ans, dfs(r, c+1, mask, sub, row) + a[r][c])
  # a[r][c]设置为0。若原来a[r][c]=1,ans+1
    dp[r][c][mask][sub][row] = ans        # 存储结果
    return ans
for i in range(9):
    s = input().strip()
    for j in range(9):   a[i][j] = s[j] == '1' # 存到 a[0][0]~a[8][8]
print(dfs(0, 0, 0, 0, False))

おすすめ

転載: blog.csdn.net/weixin_43914593/article/details/132795227