Tag Archives: 剪枝

LeetCode Matchsticks to Square

LeetCode Matchsticks to Square Remember the story of Little Match Girl? By now, you know exactly what matchsticks the little match girl has, please find out a way you can make one square by using up all those matchsticks. You should not break any stick, but you can link them up, and each matchstick must be used exactly one time. Your input will be several matchsticks the girl has, represented with their stick length. Your output will either be true or false, to represent whether you could make one square using all the matchsticks the little match girl has. Example 1:

Input: [1,1,2,2,2]
Output: true
Explanation: You can form a square with length 2, one side of the square came two sticks with length 1.
Example 2:
Input: [3,3,3,3,4]
Output: false
Explanation: You cannot find a way to form a square with all the matchsticks.
Note:
  1. The length sum of the given matchsticks is in the range of 0 to 10^9.
  2. The length of the given matchstick array will not exceed 15.

卖火柴的小女孩有一堆长度不等的火柴棒,问用这些火柴棒能否围成一个正方形。 因为最多有15根火柴棒,所以可以DFS求解。 首先判断所有火柴棒的长度之和是否为4的倍数,如果是,则和除以4等于edge,也就是我们的目标是把所有火柴棒分成和等于edge的四等份。DFS的思路就是尝试把每根火柴棒放到第一、二、三、四份中,不断的递归求解。但是需要一些剪枝策略,否则会TLE。 先上代码: [cpp] class Solution { private: bool dfs(const vector<int>& nums, int idx, const int &edge, vector<int>& sums) { if (idx == nums.size() && sums[0] == sums[1] && sums[1] == sums[2] && sums[2] == sums[3])return true; if (idx == nums.size())return false; for (int i = 0; i < 4; ++i) { if (sums[i] + nums[idx] <= edge) { // (2) int j = i; while (–j >= 0) { if (sums[i] == sums[j])break; // (3) } if (j != -1)continue; sums[i] += nums[idx]; if (dfs(nums, idx + 1, edge, sums))return true; sums[i] -= nums[idx]; } } return false; } public: bool makesquare(vector<int>& nums) { int n = nums.size(); if (n < 4)return false; sort(nums.begin(), nums.end(),greater<int>()); // (1) int sum = 0; for (int i = 0; i < n; ++i)sum += nums[i]; if (sum % 4 != 0)return false; vector<int> sums = { 0,0,0,0 }; return dfs(nums, 0, sum / 4, sums); } }; [/cpp] 本代码提交AC,用时6MS。 第(1)个剪枝策略的含义是先对所有火柴棒的长度从大到小排序,为什么从大到小排序呢,因为我们的dfs过程中的for循环总是首先尝试把每根火柴棒放到第一个组份中,如果本身无解,则从小到大的策略会慢慢累积和,直到最后加上很长的火柴棒才会发现不满足if语句无解;而如果从大到小排序的话,一开始的累加和就很大,如果无解,则很快能发现不满足if。 第(2)个剪枝策略的含义是只有当把这根火柴棒加到这个组份中不会超过预定的edge,才把它加入,然后递归。这个很显然的。 第(3)个剪枝策略参考讨论区,如果递归的时候发现某一个组份当前的和等于之前某个组份的和,因为之前那个组份已经递归过了,所以本质上这次递归会和上次递归一样,可以continue掉。]]>

POJ 2676-Sudoku

POJ 2676-Sudoku Sudoku Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 14104 Accepted: 6963 Special Judge Description Sudoku is a very simple task. A square table with 9 rows and 9 columns is divided to 9 smaller squares 3×3 as shown on the Figure. In some of the cells are written decimal digits from 1 to 9. The other cells are empty. The goal is to fill the empty cells with decimal digits from 1 to 9, one digit per cell, in such way that in each row, in each column and in each marked 3×3 subsquare, all the digits from 1 to 9 to appear. Write a program to solve a given Sudoku-task. Input The input data will start with the number of the test cases. For each test case, 9 lines follow, corresponding to the rows of the table. On each line a string of exactly 9 decimal digits is given, corresponding to the cells in this line. If a cell is empty it is represented by 0. Output For each test case your program should print the solution in the same format as the input data. The empty cells have to be filled according to the rules. If solutions is not unique, then the program may print any one of them. Sample Input 1 103000509 002109400 000704000 300502006 060000050 700803004 000401000 009205800 804000107 Sample Output 143628579 572139468 986754231 391542786 468917352 725863914 237481695 619275843 854396127 Source Southeastern Europe 2005


这一题是练习深度搜索和剪枝的好题目。题意很简答,数独游戏,在每一行、每一列和每一个小正方形填入1-9这9个数字,数字不能重复,求一种可以的方案。 因为只需要求一种可行解,根据POJ Problem 3278: Catch That Cow后面的总结,很快有了深度搜索的思路:从上到下,从左到右依次扫描每一个格子,如果这个格子上的数字不是0,说明原来就有数字了,不用填,测试下一个格子;如果这个格子为0,则求出可填入该格子的候选数字,再依次测试每一个候选数字是否可行,一直这样深度搜索下去,直到将所有格子测试完毕。这个思路相信很多人都能很快想到,完整的代码如下: [cpp] #include<iostream> #include<vector> using namespace std; const int N=9; int board[N][N];//棋盘 int candidates[N];//对于某一个坐标ij,可以填入的候选数字 //给定坐标(row,column),求它所在的小方格的左上角坐标 void get_subquare_start(int row,int column,int& x0,int& y0) { if(row<3&&column<3) { x0=0; y0=0; } else if(row<3&&column<6) { x0=0; y0=3; } else if(row<3&&column<9) { x0=0; y0=6; } else if(row<6&&column<3) { x0=3; y0=0; } else if(row<6&&column<6) { x0=3; y0=3; } else if(row<6&&column<9) { x0=3; y0=6; } else if(row<9&&column<3) { x0=6; y0=0; } else if(row<9&&column<6) { x0=6; y0=3; } else if(row<9&&column<9) { x0=6; y0=6; } } //给定坐标(row,column),将可以填入该坐标的候选数字放入vi //思路就是先把candidates[N]填满1-9,然后将给定坐标的 //横轴、纵轴、小方格出现过的数字去掉,剩下的数字就是 //候选数字 void get_candidates(int row,int column,vector<int>& vi) { for(int i=0;i<N;i++) candidates[i]=i+1; for(int i=0;i<N;i++)//横轴 { if(board[row][i]!=0) candidates[board[row][i]-1]=0; } for(int i=0;i<N;i++)//纵轴 { if(board[i][column]!=0) candidates[board[i][column]-1]=0; } int x0,y0; get_subquare_start(row,column,x0,y0); for(int i=x0;i<x0+3;i++)//小方格 { for(int j=y0;j<y0+3;j++) { if(board[i][j]!=0) candidates[board[i][j]-1]=0; } } for(int i=0;i<N;i++)//重复工作,可优化 if(candidates[i]!=0) vi.push_back(candidates[i]); } //深度搜索 bool dfs(int pos) { if(pos==N*N) { return true; } int row=pos/N; int column=pos%N; if(board[row][column]!=0)//如果有数字 return dfs(pos+1);//则搜下一个格子 else { vector<int> local_candidates;//要使用局部的candidates,因为如果是全局的,那么回溯的时候candidates已经修改了 get_candidates(row,column,local_candidates);//获取可填入该格子的候选数字 int cand_num=local_candidates.size(); for(int i=0;i<cand_num;i++) { board[row][column]=local_candidates[i];//填入 if(dfs(pos+1))//继续搜下一个格子 return true; } board[row][column]=0;//回溯 return false;//回溯 } } int main() { freopen("input.txt","r",stdin); int t; char c; cin>>t; while(t–) { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) { cin>>c; board[i][j]=c-‘0’; } } if(dfs(0)) { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) cout<<board[i][j]; cout<<endl; } } } return 0; } [/cpp] 满心欢喜的提交后发现TLE超时了,此时泪流满面,后来又琢磨了一下,师兄给的分类是简单搜索技巧和剪枝,我上面的代码压根就没有剪枝,TLE也就不足为奇了。 那么怎样剪枝?想一想我们手算解数独的过程,正常情况下,如果A这个格子有5个候选数字可以填入,而B这个格子只有2个候选数字可以填入,我们肯定会先填好B这个格子,再填A,因为B的情况少一些,而填完B之后,A的情况可能又会减少,这样比先填A再填B会快得多。根据这样的思路,我们在每次深度搜索的时候,把每个空格的候选数字求出来,并求候选数字个数最少的那个格子C,填格子C,继续深度搜索,直到所有空格填满为止。 可能有的同学会问,每次深度搜索的时候都要求一遍所有空格的候选数字,这样会不会耗时太多了,这个我也担心过,但是总体来说,剪枝比第一个版本的代码高效多了。下面是剪枝版本的完整代码: [cpp] #include<iostream> #include<vector> using namespace std; const int N=9; int board[N][N];//棋盘 int candidates[N];//对于某一个坐标ij,可以填入的候选数字 //给定坐标(row,column),求它所在的小方格的左上角坐标 void get_subquare_start(int row,int column,int& x0,int& y0) { if(row<3&&column<3) { x0=0; y0=0; } else if(row<3&&column<6) { x0=0; y0=3; } else if(row<3&&column<9) { x0=0; y0=6; } else if(row<6&&column<3) { x0=3; y0=0; } else if(row<6&&column<6) { x0=3; y0=3; } else if(row<6&&column<9) { x0=3; y0=6; } else if(row<9&&column<3) { x0=6; y0=0; } else if(row<9&&column<6) { x0=6; y0=3; } else if(row<9&&column<9) { x0=6; y0=6; } } //给定坐标(row,column),将可以填入该坐标的候选数字放入vi //思路就是先把candidates[N]填满1-9,然后将给定坐标的 //横轴、纵轴、小方格出现过的数字去掉,剩下的数字就是 //候选数字 void get_candidates(int row,int column,vector<int>& vi) { for(int i=0;i<N;i++) candidates[i]=i+1; for(int i=0;i<N;i++)//横轴 { if(board[row][i]!=0) candidates[board[row][i]-1]=0; } for(int i=0;i<N;i++)//纵轴 { if(board[i][column]!=0) candidates[board[i][column]-1]=0; } int x0,y0; get_subquare_start(row,column,x0,y0); for(int i=x0;i<x0+3;i++)//小方格 { for(int j=y0;j<y0+3;j++) { if(board[i][j]!=0) candidates[board[i][j]-1]=0; } } for(int i=0;i<N;i++)//重复工作,可优化 if(candidates[i]!=0) vi.push_back(candidates[i]); } //深度搜索 bool dfs(vector<int>& blank_pos) { int blank_num=blank_pos.size(); if(blank_num==0)//如果空格都填完了,返回true { return true; } int row,column,pos; int min_candidates=N+1; vector<int> global_candidates; for(int i=0;i<blank_num;i++) { row=blank_pos[i]/N; column=blank_pos[i]%N; vector<int> local_candidates; get_candidates(row,column,local_candidates);//对于每一个空格求可填入的候选数字 if(local_candidates.size()<min_candidates)//求到最小值 { pos=blank_pos[i];//记录位置 min_candidates=local_candidates.size(); global_candidates.clear(); global_candidates=local_candidates;//vector可以直接赋值 } } row=pos/N; column=pos%N; vector<int> next_blank_pos; for(int j=0;j<blank_num;j++) if(blank_pos[j]!=pos) next_blank_pos.push_back(blank_pos[j]);//剩余的空格 for(int i=0;i<min_candidates;i++) { board[row][column]=global_candidates[i];//填入 if(dfs(next_blank_pos))//继续搜下一个格子 return true; } board[row][column]=0;//回溯 return false;//回溯 } //剪枝版本,AC,用时282MS,内存240K int main() { //freopen("input.txt","r",stdin); int t; char c; cin>>t; vector<int> blank_pos; while(t–) { blank_pos.clear();//因为可能有多个case,所以每次都要clear!!! for(int i=0;i<N;i++) { for(int j=0;j<N;j++) { cin>>c; board[i][j]=c-‘0’; if(board[i][j]==0) blank_pos.push_back(i*N+j); } } if(dfs(blank_pos)) { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) cout<<board[i][j]; cout<<endl; } } } return 0; } [/cpp] 这个版本的代码需要注意一个小细节,我在每次输入的时候都将空格的位置记录在了blank_pos的vector中,因为题目说有多个case,所以每次输入的时候都要记得blank_pos.clear(),要不然会影响下一个case。这个版本的代码AC,用时282MS,内存240K。 AC之后,又看了一下discuss,发现很多同学谈到如果倒搜,会比顺搜快得多,倒搜就是从最后一个格子开始搜索。闲来无事,我就把第一个版本的代码改成了倒搜索,只需要修改两三个地方,代码如下: [cpp] #include<iostream> #include<vector> using namespace std; const int N=9; int board[N][N];//棋盘 int candidates[N];//对于某一个坐标ij,可以填入的候选数字 //给定坐标(row,column),求它所在的小方格的左上角坐标 void get_subquare_start(int row,int column,int& x0,int& y0) { if(row<3&&column<3) { x0=0; y0=0; } else if(row<3&&column<6) { x0=0; y0=3; } else if(row<3&&column<9) { x0=0; y0=6; } else if(row<6&&column<3) { x0=3; y0=0; } else if(row<6&&column<6) { x0=3; y0=3; } else if(row<6&&column<9) { x0=3; y0=6; } else if(row<9&&column<3) { x0=6; y0=0; } else if(row<9&&column<6) { x0=6; y0=3; } else if(row<9&&column<9) { x0=6; y0=6; } } //给定坐标(row,column),将可以填入该坐标的候选数字放入vi //思路就是先把candidates[N]填满1-9,然后将给定坐标的 //横轴、纵轴、小方格出现过的数字去掉,剩下的数字就是 //候选数字 void get_candidates(int row,int column,vector<int>& vi) { for(int i=0;i<N;i++) candidates[i]=i+1; for(int i=0;i<N;i++)//横轴 { if(board[row][i]!=0) candidates[board[row][i]-1]=0; } for(int i=0;i<N;i++)//纵轴 { if(board[i][column]!=0) candidates[board[i][column]-1]=0; } int x0,y0; get_subquare_start(row,column,x0,y0); for(int i=x0;i<x0+3;i++)//小方格 { for(int j=y0;j<y0+3;j++) { if(board[i][j]!=0) candidates[board[i][j]-1]=0; } } for(int i=0;i<N;i++)//重复工作,可优化 if(candidates[i]!=0) vi.push_back(candidates[i]); } //深度搜索 bool dfs(int pos) { if(pos==-1)//倒搜到-1结束 { return true; } int row=pos/N; int column=pos%N; if(board[row][column]!=0)//如果有数字 return dfs(pos-1);//则搜下一个格子 else { vector<int> local_candidates;//要使用局部的candidates,因为如果是全局的,那么回溯的时候candidates已经修改了 get_candidates(row,column,local_candidates);//获取可填入该格子的候选数字 int cand_num=local_candidates.size(); for(int i=0;i<cand_num;i++) { board[row][column]=local_candidates[i];//填入 if(dfs(pos-1))//继续搜下一个格子 return true; } board[row][column]=0;//回溯 return false;//回溯 } } //倒搜版本,AC,用时125MS,内存224K int main() { //freopen("input.txt","r",stdin); int t; char c; cin>>t; while(t–) { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) { cin>>c; board[i][j]=c-‘0’; } } if(dfs(N*N-1))//倒搜 { for(int i=0;i<N;i++) { for(int j=0;j<N;j++) cout<<board[i][j]; cout<<endl; } } } return 0; } [/cpp] 令我吃惊的是,倒搜这个版本的代码AC了,而且用时125MS,内存224K,居然比剪枝还要快,内存占用也更少。到现在我还没有搞清楚更快的原因。]]>