問題
在N*M的草地上,提莫種了K個蘑菇,蘑菇爆炸的威力極大,蘭博不想貿然去闖,而且蘑菇是隱形的.只 有一種叫做掃描透鏡的物品可以掃描出隱形的蘑菇,於是他回了一趟戰爭學院,買了2個掃描透鏡,一個 掃描透鏡可以掃描出(3*3)方格中所有的蘑菇,然後蘭博就可以清理掉一些隱形的蘑菇. 問:蘭博最多可以清理多少個蘑菇?
注意:每個方格被掃描一次只能清除掉一個蘑菇。
輸入描述
第一行三個整數:N,M,K,(1≤N,M≤20,K≤100),N,M代表了草地的大小;
接下來K行,每行兩個整數x,y(1≤x≤N,1≤y≤M).代表(x,y)處提莫種了一個蘑菇.
一個方格可以種無窮個蘑菇.
輸出描述
輸出一行,在這一行輸出一個整數,代表蘭博最多可以清理多少個蘑菇.
Java Code
import java.util.ArrayList;
import java.util.Scanner;
public class LensScanner {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
while (scan.hasNext()) {
int m = scan.nextInt();
int n = scan.nextInt();
int k = scan.nextInt();
int[][] mushroom = new int[k][2];
for (int i = 0; i < k; ++i) {
mushroom[i][0] = scan.nextInt();
mushroom[i][1] = scan.nextInt();
}
System.out.println(solve(m, n, k, mushroom));
}
scan.close();
}
public static int solve(int m, int n, int k, int[][] mushroom) {
// 構造一塊(m+2)*(n+2)大小的草坪,左上角爲原點
int[][] graph = new int[m + 2][n + 2];
// 將k個蘑菇種在草坪上,注意蘑菇的橫/縱座標均從1開始
for (int i = 0; i < k; ++i) {
graph[mushroom[i][0]][mushroom[i][1]]++;
}
// 兩個變量用於記錄兩次掃描中分別最多可以清理掉的蘑菇數目
int max1 = 0;
int max2 = 0;
// 數組用於記錄已遍歷的九宮格區域內“有蘑菇的格子”個數最多的位置(可能同時存在多個)
ArrayList<Integer> coordinate = new ArrayList<Integer>();
// 遍歷整個草坪,統計每個格子周圍3*3方塊內有蘑菇的格子個數(不是九宮格內蘑菇的總數!)
// 草坪最外圍的一圈格子不計算,因爲他們是虛構出來的padding區域
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
// 第一個透鏡掃描一個九宮格
int temp = 0;
for (int dx = -1; dx < 2; ++dx) {
for (int dy = -1; dy < 2; ++dy) {
if (graph[i + dx][j + dy] > 0)
temp++;
}
}
if (temp > max1) {
max1 = temp;// 找到了更大的值,更新max1
coordinate.clear();// 清空之前存儲的座標信息
coordinate.add(i);
coordinate.add(j);
} else if (temp == max1) {
coordinate.add(i);// 如果存在多個最大值位置,全部記錄之
coordinate.add(j);
}
}
}
// 對於max1對應的每個座標位置,遍歷查找最大的max2
for (int idx = coordinate.size() - 1; idx >= 0;) {
// 取出某個最大值位置
int y1 = coordinate.get(idx--);
int x1 = coordinate.get(idx--);
// 先構造一個(m+2)*(n+2)大小的掩膜,
// 將第一個透鏡掃描過的九個格子裏的蘑菇都分別減1(不論該格子裏有沒有蘑菇)
int[][] mask = new int[m + 2][n + 2];
for (int dx = -1; dx < 2; ++dx) {
for (int dy = -1; dy < 2; ++dy) {
mask[x1 + dx][y1 + dy]--;
}
}
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
// 第二個透鏡掃描一個九宮格
int temp = 0;
for (int dx = -1; dx < 2; ++dx) {
for (int dy = -1; dy < 2; ++dy) {
// 一個格子裏至少有2個蘑菇才能在第二次被掃描時再次被清除
if (graph[i + dx][j + dy] + mask[i + dx][j + dy] > 0)
temp++;
}
}
// 得到第二次掃描最多能清除掉的蘑菇數目max2
if (temp > max2)
max2 = temp;
// 可能提前結束掃描
if (18 == max1 + max2)
return 18;
}
}
}
return (max1 + max2);
}
}
說明
本題的所提的問題本身並不複雜,而且需求很清晰,但是由於條件限制,實現起來並不太容易。首先就是m或n可能小於3,這樣每個格子都沒有完整的8鄰域,而且即使m和n都大於3,草坪最外圍一圈格子也會沒有完整的8鄰域,所以這裏我們可以給整個草坪(下圖深色底紋的格子)的外圍再加上一圈padding格子(下圖中的淺色虛線格子),這樣就能保證原草坪中的每個格子都有8鄰域,方便我們用統一的公式計算。可以畫個示意圖如下:
由於題目所給蘑菇的座標正好也是從1開始的,所以padding格子中不會有蘑菇。題目要求兩次掃描最多可以清除掉的蘑菇總數,而且每次掃描只能清除掉一個格子中的一個蘑菇,所以每次掃描最多可以清除9個蘑菇,每個格子最多可以被清除掉2個蘑菇。上述代碼使用的是貪婪算法,每次都儘可能多地清除蘑菇,由於第一次符合條件的掃描位置可能不止一個(比如上圖中的4個紅色位置都能在一次掃描中清除掉最多5個蘑菇,但是其中只有2個位置能使第二次掃描清除掉最多4個蘑菇),所以還要針對所有第一次掃描的候選位置,結合第二次掃描的結果來得到兩次掃描的最大清除量。本解法的時間複雜度爲O(m·n)+k·O(m·n)=(1+k)·O(m·n),其中k爲第一個透鏡掃描的所有候選位置,即代碼中max1對應的所有座標位置,所以k是不是常量級呢?
本題的解法雖然通過了牛客網的OJ,但是用貪婪算法分別求兩次掃描的最大清除量之和就一定是總的全局最大清除量嗎?
更新(2016.08.05)
牛客網有同學提到,分兩步單獨搜索所得到的結果只是局部最優解,並舉出了如下的例子說明,當m=3,n=8時,假如蘑菇分佈如下:
在第一個透鏡掃描時,最大的可清除數是6個,共有4種位置,如果選擇清除掉中間的6個蘑菇,那麼第二次最多清除掉3個蘑菇,這樣就不能全清所有蘑菇了。而我上面提供的代碼,把這4種情況都對第二次掃描做了聯合求解,結果是能夠清除掉所有蘑菇的,所以在本題使用貪婪算法並不一定不能得到全局最優解。但是我無法證明這一點,希望有同學能提供證明方法。另外,爲了避免局部最優解,可以使用下述代碼的暴力搜索方式,處理的大致思路和之前的代碼是類似的,也需要構造padding格子,只是每次用第一個透鏡掃描時都要結合第二個透鏡掃描的結果,這樣得到的必定是全局最優解,但是算法複雜度達到了O((m·n)^2)。如果有同學有更優雅的解法,歡迎交流學習!
Java Code
public static int brutalSolve(int m, int n, int k, int[][] mushroom) {
// 構造一塊(m+2)*(n+2)大小的草坪,左上角爲原點
int[][] graph = new int[m + 2][n + 2];
// 將k個蘑菇種在草坪上,注意蘑菇的橫/縱座標均從1開始
for (int i = 0; i < k; ++i) {
graph[mushroom[i][0]][mushroom[i][1]]++;
}
// 記錄兩次掃描中最多可以清理掉的總蘑菇數目
int max = 0;
// 同時使用兩個透鏡遍歷整個草坪,使能夠清除的蘑菇數最大
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
// 先構造一個(m+2)*(n+2)大小的掩膜,表示第一個透鏡掃描後被清除掉的蘑菇位置
int[][] mask = new int[m + 2][n + 2];
// 第一個透鏡掃描一個九宮格
int max1 = 0;
for (int dx1 = -1; dx1 < 2; ++dx1) {
for (int dy1 = -1; dy1 < 2; ++dy1) {
if (graph[i + dx1][j + dy1] > 0) {
max1++;//該位置有蘑菇,清除量加1
mask[i + dx1][j + dy1]--;//該位置蘑菇數減1
}
}
}
for (int r = 1; r <= m; ++r) {
for (int t = 1; t <= n; ++t) {
// 第二個透鏡掃描一個九宮格
int max2 = 0;
for (int dx2 = -1; dx2 < 2; ++dx2) {
for (int dy2 = -1; dy2 < 2; ++dy2) {
if (graph[r + dx2][t + dy2] + mask[r + dx2][t + dy2] > 0)
max2++;
}
}
// 更新max
if (max1 + max2 > max)
max = max1 + max2;
// 可能提前結束掃描
if (18 == max)
return 18;
}
}
}
}
return max;
}