1. 程式人生 > 其它 >[luogu p1074] 靶型數獨

[luogu p1074] 靶型數獨

技術標籤:演算法dfs動態規劃字串雜湊表

傳送門

題面

題目描述

小城和小華都是熱愛數學的好學生,最近,他們不約而同地迷上了數獨遊戲,好勝的他們想用數獨來一比高低。但普通的數獨對他們來說都過於簡單了,於是他們向 Z 博士請教,Z 博士拿出了他最近發明的"靶形數獨",作為這兩個孩子比試的題目。

靶形數獨的方格同普通數獨一樣,在 \(9\) 格寬×\(9\) 格高的大九宮格中有\(9\)\(3\) 格寬×\(3\) 格高的小九宮格(用粗黑色線隔開的)。在這個大九宮格中,有一些數字是已知的,根據這些數字,利用邏輯推理,在其他的空格上填入 \(1\)

\(9\)的數字。每個數字在每個小九宮格內不能重複出現,每個數字在每行、每列也不能重複出現。但靶形數獨有一點和普通數獨不同,即每一個方格都有一個分值,而且如同一個靶子一樣,離中心越近則分值越高。(如圖)

上圖具體的分值分佈是:最裡面一格(黃色區域)為 \(10\) 分,黃色區域外面的一圈(紅色區域)每個格子為\(9\)分,再外面一圈(藍色區域)每個格子為$ 8$ 分,藍色區域外面一圈(棕色區域)每個格子為\(7\) 分,最外面一圈(白色區域)每個格子為\(6\)分,如上圖所示。比賽的要求是:每個人必須完成一個給定的數獨(每個給定數獨可能有不同的填法),而且要爭取更高的總分數。而這個總分數即每個方格上的分值和完成這個數獨時填在相應格上的數字的乘積的總和。

總分數即每個方格上的分值和完成這個數獨時填在相應格上的數字的乘積的總和。如圖,在以下的這個已經填完數字的靶形數獨遊戲中,總分數為 2829。遊戲規定,將以總分數的高低決出勝負。

由於求勝心切,小城找到了善於程式設計的你,讓你幫他求出,對於給定的靶形數獨,能夠得到的最高分數。

輸入輸出格式

輸入格式

一共 \(9\) 行。每行$ 9 $個整數(每個數都在 \(0-9\) 的範圍內),表示一個尚未填滿的數獨方格,未填的空格用"\(0\)"表示。每兩個數字之間用一個空格隔開。

輸出格式

輸出共 \(1\) 行。輸出可以得到的靶形數獨的最高分數。如果這個數獨無解,則輸出整數\(-1\)

輸入輸出樣例

輸入樣例 #1

7 0 0 9 0 0 0 0 1
1 0 0 0 0 5 9 0 0
0 0 0 2 0 0 0 8 0
0 0 5 0 2 0 0 0 3
0 0 0 0 0 0 6 4 8
4 1 3 0 0 0 0 0 0
0 0 7 0 0 2 0 9 0
2 0 1 0 6 0 8 0 4
0 8 0 5 0 4 0 1 2

輸出樣例 #1

2829

輸入樣例 #2

0 0 0 7 0 2 4 5 3
9 0 0 0 0 8 0 0 0
7 4 0 0 0 5 0 1 0
1 9 5 0 8 0 0 0 0
0 7 0 0 0 0 0 2 5
0 3 0 5 7 9 1 0 8
0 0 0 6 0 1 0 0 0
0 6 0 9 0 0 0 0 1
0 0 0 0 0 0 0 0 6

輸出樣例 #2

2852

說明

【資料範圍】\(40\%\)的資料,數獨中非 \(0\) 數的個數不少於\(30\)\(80\%\)的資料,數獨中非 \(0\) 數的個數不少於\(26\)\(100\%\)的資料,數獨中非\(0\)數的個數不少於\(24\)NOIP 2009 提高組 第四題

分析

這是一道很有趣的技巧性搜尋,看了下資料範圍和時限,好像暴搜有點危險,自己就加了一個優化:找空地少的一行開始搜
啥意思呢,我們平常做數獨的時候,一般都是從填的數最少的那一行開始,如果程式我們用這個優化,會減掉好多枝。美滋滋。
然後就是一些細節問題了,比如上面所說的優化,還有分數的計算什麼的,在這些方面我踩了好多坑,debug了很長時間。不過多踩點坑總是好的。
具體請見程式碼,註釋個人認為比較詳盡。
另外,從主函式開始閱讀程式碼是個好習慣,否則你可能會雲裡霧裡。

程式碼

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2020-02-23 13:23:05 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2020-02-23 16:03:12
 */
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>

const int maxn = 15;
int sudoku[maxn][maxn],score[maxn][maxn];//數獨和每個位置對應的分數
int first[85];
//這個first有點難理解,對first[i],i表示順序,i越小,掃到的點越靠前。
//而first[i]是一個int值,用來存放第i個搜尋到的點在數獨中的位置,這裡所說的位置
//已經經過了對映,也就是將數獨中點的位置的二維對映到一維。
//對映方法很簡單,(x-1)*9+y
int ans;//存放答案
bool visc[maxn][maxn],visr[maxn][maxn],visg[maxn][maxn];
//標記每一行,每一列,每一宮的訪問情況。具體:
//比如visc[i][j]表示第i行中數j是否出現。
//根據英文來說,visc是列,visr是行,但是本程式中全都寫反了,也就懶得改了。
bool fnd;//標記有無解

inline int max(int a,int b) {
    return a > b ? a : b;
}

struct row {
    int ind,zero;
}r[maxn];

bool cmp(row a,row b) {
    return a.zero < b.zero;
}

int cal() {
    int ret = 0;
    for(int i = 1; i <= 9; i++) 
        for(int j = 1; j <= 9; j++)
            ret += sudoku[i][j] * score[i][j];//計算每個位置的分數,並進行累加
    return ret;
}

void dfs(int step) {
    if(step > 81) {//搜完了
        fnd = true;//表示有解
        ans = max(ans,cal());//更新答案
        return ;
    }
    
    int x = first[step] / 9 + 1;
    int y = first[step] % 9;
    if(!y) {
        x--;
        y = 9;
    }
    //一維對映到二維。
    if(sudoku[x][y]) dfs(step + 1);
    //如果sudoku[x][y]成立說明當前的數不是0,也就是當前位置是已知的數,不能更改。
    //我們直接跳過去,搜尋下一個就好、
    else {
        for(int i = 1; i <= 9; i++) {
            if(!visc[x][i] && !visr[y][i] && !visg[(x - 1) / 3 * 3 + (y - 1) / 3 + 1][i]) {//判斷當前位置填這個數是否合法
                sudoku[x][y] = i;
                visc[x][i] = visr[y][i] = visg[(x - 1) / 3 * 3 + (y - 1) / 3 + 1][i] = true;
                //標記
                dfs(step + 1);//遞迴
                sudoku[x][y] = 0;
                visc[x][i] = visr[y][i] = visg[(x - 1) / 3 * 3 + (y - 1) / 3 + 1][i] = false;        
                //回溯        
                //printf("lalalalala\n");
            }
        }
    }
}

int main() {
    for(int i = 1; i <= 9; i++) {
        int zero = 0;//記錄當前行中0的數量。
        for(int j = 1; j <= 9; j++) {
            scanf("%d",&sudoku[i][j]);
            if(!sudoku[i][j]) zero++;
            else {
                visc[i][sudoku[i][j]] = true;
                visr[j][sudoku[i][j]] = true;
                visg[(i - 1) / 3 * 3 + (j - 1) / 3 + 1][sudoku[i][j]] = true;         
                //這裡千萬別落寫,我在這裡卡了半小時,,(慚愧.jpg)
            }
        }
        r[i].ind = i;//這裡要記錄一下,因為一會要排序,行數會丟失。
        r[i].zero = zero;//記錄當前行中0的數量
    }
    std :: sort(r+1,r+10,cmp);//對行進行排序
        
    for(int i = 1; i <= 9; i++) {
        for(int j = 1; j <= 9; j++) {
            int x = r[i].ind;
            int y = j;
            first[i * 9 - 9 + j] = x * 9 - 9 + y;
            //標記位置。
        }
    }

    for(int i = 1,j = 10 - i; i <= 5; i++,j--)
        for(int k = i; k <= j; k++)
            score[i][k] = score [k][i] = score[k][j] = score[j][k] = i + 5;
    //預處理score陣列,便於計算總分
    dfs(1);//進行dfs
    if(fnd) printf("%d\n",ans);
    else printf("-1\n");
    //輸出答案
    return 0;//結束程式
}

評測結果

AC 100R30975285

over.