UVa Problem 10270 Bigger Square Please... (拼接正方形)

  1. // Bigger Square Please... (拼接正方形)   
  2. // PC/UVa IDs: 110808/10270, Popularity: C, Success rate: high Level: 3   
  3. // Verdict: Accepted   
  4. // Submission Date: 2011-09-25   
  5. // UVa Run Time: 0.032s   
  6. //   
  7. // 版权所有(C)2011,邱秋。metaphysis # yeah dot net   
  8. //   
  9. // [问题描述]   
  10. // Tony 有很多张正方形纸片。这些纸片的边长为 1 到 N - 1 不等,且每种纸片都有无数张。但是他并不   
  11. // 满足。他想要一张更大的 ---- 边长为 N 的纸片。   
  12. //   
  13. // 可以把已有纸片拼接成他想要的大正方形。例如,一个边长为 7 的正方形可以通过如下 9 个更小的正方形   
  14. // 拼接而成(使用字母来填充相应的正方形,A 表示边长为 1 的正方形纸片,B 表示边长为 2 的正方形纸   
  15. // 片,依此类推):   
  16. //   
  17. //                B B B B C C C    
  18. //                B B B B C C C    
  19. //                A B B A C C C    
  20. //                A B B D D D D    
  21. //                C C C D D D D    
  22. //                C C C D D D D    
  23. //                C C C D D D D   
  24. //   
  25. // 在拼接出的正方形中间不能有空隙,不能有纸片超出正方形,且纸片不能相互重叠。并且,Tony 想要用尽   
  26. // 可能少的纸片来拼出这个大的正方形。你能帮助他吗?   
  27. //   
  28. // [输入]   
  29. // 输入第一行有一个单独的整数 T,表示测试数据的组数。每组数据为一个单独的整数 N(2 <= N <= 50)。   
  30. //   
  31. // [输出]   
  32. // 对于每组数据,输出一行,包含一个整数 K,表示最少需要的纸片数。接下来 K 行,每行三个整数 x,y,   
  33. // l,表示纸片左上角的坐标 (1 <= x,y <= N) 以及纸片的边长。   
  34. //   
  35. // [样例输入]   
  36. // 3   
  37. // 4   
  38. // 3   
  39. // 7   
  40. //   
  41. // [样例输出]   
  42. // 4   
  43. // 1 1 2   
  44. // 1 3 2   
  45. // 3 1 2   
  46. // 3 3 2   
  47. // 6   
  48. // 1 1 2   
  49. // 1 3 1   
  50. // 2 3 1   
  51. // 3 1 1   
  52. // 3 2 1   
  53. // 3 3 1   
  54. // 9   
  55. // 1 1 2   
  56. // 1 3 2   
  57. // 3 1 1   
  58. // 4 1 1   
  59. // 3 2 2   
  60. // 5 1 3   
  61. // 4 4 4   
  62. // 1 5 3   
  63. // 3 4 1   
  64. //   
  65. // [解题方法]   
  66. // 该题是 UVa 上的所有题目中 10% 较难的题目中的一题。如果是实时产生拼接方案,而不是预先生成拼接   
  67. // 方案再提交,说明水平确实比较高。题目要求用边长为 1 - (N - 1) 的任意张纸片拼接成一个边长为   
  68. // N 的正方形纸片,纸片间互相不能重叠,且不能超出边长为 N 的正方形范围,求使用纸片数最少的拼接方   
  69. // 案,以左上角为坐标起点 (1,1),按坐标、纸张边长的顺序输出每张纸片的坐标、边长值。   
  70. //   
  71. // 根据题意,设需要拼接的正方形边长为 N,很明显,拼接方案是一系列平方数的和。问题转化为如何将 N 表   
  72. // 示为平方数之和,且要求平方数的个数最小,这个问题可以通过回溯来解决。但是得到了一个将 N 拆分为平   
  73. // 方数之和的方案,并不表示就能将这些大小的纸片拼接成一个边长为 N 的纸片,例如,对于 N = 5,可以   
  74. // 拆分为 9 与 16 的和,9 和 16 都是平方数,但是实际上无法将一张边长为 3 和一张边长为 4 的纸片   
  75. // 拼接为一张边长为 5 的纸片,所以在生成一个平方数和方案后,需要实际尝试放置,若能放置,则表明此方   
  76. // 案可行,予以记录,将所有可行的方案记录后,挑选其中纸片数最小的方案即为所求。这样的话,将 N 拆分   
  77. // 为平方数之和是一步回溯,将拆分方案尝试放置又是一步回溯,需要通过两步回溯来解决本问题。   
  78. //   
  79. // 考虑到需要两步回溯,如果不予以充分剪枝,则计算时间将不可忍受,在 UVa BBS 上关于这个题目的讨论   
  80. // 即反映了这种情况。在将 N 拆分为平方数和的一步,若能生成一个拆分方案,尽管不是最优的,但是它的总   
  81. // 个数较小,且能实际放置,则将此方案的总个数作为剪枝阈值将可避免较多无效的搜索,将 N 拆分为平方数   
  82. // 后且能实际放置的一个非最优方案可以这样构造:左上角放置一个边长为 (N - 2) 的纸片,然后在右侧和   
  83. // 下方放置边长为 2 的纸片,剩余的空间放置边长为 1 的纸片,这样总的纸片需要数量为: 1 + [N / 2]   
  84. // + [(N - 2) / 2] + 4,其中符号 [] 表示取整,这样总的纸片放置数在 N 附近,可以做为一个较好   
  85. // 的剪枝阈值。通过回溯发现了总个数更少的方案后,则开始尝试实际放置,可以通过设立一个网格数组,当   
  86. // 填充边长为 A 的纸片时,在网格中查找是否有起始坐标为 (x,y) 且边长为 A 的空白区域,若无此种   
  87. // 空白区域,则表明该方案无法实际放置,若能找到,则找到所有这样的起始位置,逐一回溯进行尝试,尝试   
  88. // 某位置后,则将该区域标记为已填充,若后继填充不成功返回时则撤销标记。对于已经产生但不能实际拼接   
  89. // 的拆分方案需要予以记录,在后继生成的方案中,若有方案与记录的方案相同,则不必再次浪费时间搜索。   
  90. //   
  91. // 通过网络搜索该问题的相关信息可以知道,当 N MOD 2 = 0 或者 N MOD 6 = 3 时,有特殊的解法。   
  92. // 对于 N MOD 2 = 0,即 N 为偶数时,最少纸片数的拼接方法为:用 4 张边长为 N / 2 的纸片拼接得   
  93. // 到边长为 N 的正方形。当 N MOD 6 = 3 时,放置方法与 N = 3 的方案类似,只不过将相应的纸片边长   
  94. // 增加同样的数量。同时平方数如 25 的拼接方案和 5 的拼接方案类似,49 的拼接方案和 7 的拼接方案   
  95. // 类似,则在 2 - 50 之间的数,只需要求出质数的拼接方法即可。网络上已经有 2 - 50 之间的质数的   
  96. // 拼接方案和最少需要纸片数,利用这些信息,可以显著减少计算时间,甚至直接生成拼接方案后再提交。   
  97. //   
  98. // 参考网页:   
  99. // http://www2.stetson.edu/~efriedma/mathmagic/1298.html   
  100. // http://mathpuzzle.com/perkinsbestquilts.txt   
  101. // http://mathworld.wolfram.com/MrsPerkinssQuilt.html   
  102.   
  103. #include <iostream>   
  104. #include <cstring>   
  105. #include <algorithm>   
  106. #include <ctime>   
  107.   
  108. using namespace std;  
  109.   
  110. #ifndef DEBUG_MODE   
  111. #define DEBUG_MODE  // 测试用,若在线提交,需要将该语句注释掉。   
  112. #endif   
  113.   
  114. #define NMAX 20     // 拆分时,纸片最多需要的张数。   
  115. #define NPRIME 11   // 10 - 50 之间的质数个数。   
  116. #define SMAX 1024   // 最多保存的未成功拼接的拆分方案数。   
  117. #define PMAX 2500   // 网格中坐标最大个数。   
  118. #define NCELL 50    // 网格边长。   
  119.   
  120. struct square       // 表示拼接的纸片信息。   
  121. {  
  122.     int x, y;   // 纸片在网格中的坐标。   
  123.     int size;   // 纸片的边长。   
  124. };  
  125.   
  126. square squares[NMAX];   // 记录当前的拼接方案。   
  127. square best[NMAX];  // 记录搜索得到的最好实际拼接方案。   
  128.   
  129. struct point    // 表示网格中的一个点。   
  130. {  
  131.     int x;      // 点的横坐标。   
  132.     int y;      // 点的纵坐标。   
  133. };  
  134.   
  135. // 2 - 50 之间的奇数拼接时所需要的最少纸片数。   
  136. int tip[24] = { 6, 8, 9, 6, 11, 11, 6, 12, 13, 6, 13, 8, 6, 14, 15, 6, 8, 15, 6,  
  137.     15, 16, 6, 17, 9  
  138. };  
  139.   
  140. // 10 - 50 之间的质数的最佳拆分方案。数组第一个数表示质数,第二个数表示所需纸片数,其后的数字为   
  141. // 纸片大小,按纸片从大到小排列。   
  142. int trick[NPRIME][NMAX] = {  
  143.     {11, 11, 6, 5, 5, 4, 2, 2, 2, 2, 1, 1, 1},              // 11   
  144.     {13, 11, 7, 6, 6, 4, 3, 3, 2, 2, 2, 1, 1},              // 13   
  145.     {17, 12, 9, 8, 8, 5, 4, 4, 3, 2, 2, 2, 1, 1},               // 17   
  146.     {19, 13, 10, 9, 9, 5, 5, 5, 3, 2, 2, 2, 1, 1, 1},           // 19   
  147.     {23, 13, 12, 11, 11, 7, 5, 5, 4, 3, 3, 2, 2, 1, 1},         // 23   
  148.     {29, 14, 17, 12, 12, 9, 8, 8, 4, 4, 3, 2, 2, 2, 1, 1},          // 29   
  149.     {31, 15, 16, 15, 15, 8, 8, 8, 4, 4, 4, 2, 2, 2, 1, 1, 1},       // 31   
  150.     {37, 15, 19, 18, 18, 11, 8, 8, 6, 5, 5, 3, 3, 2, 1, 1, 1},      // 37   
  151.     {41, 15, 23, 18, 18, 12, 11, 11, 7, 5, 4, 3, 3, 2, 2, 1, 1},        // 41   
  152.     {43, 16, 22, 21, 21, 11, 11, 11, 6, 5, 5, 3, 3, 3, 2, 1, 1, 1},     // 43   
  153.     {47, 17, 25, 22, 22, 13, 12, 9, 8, 8, 5, 5, 4, 3, 3, 2, 2, 1, 1},   // 47   
  154. };  
  155.   
  156. int n;              // 要拼接的正方形边长。   
  157. int smallest;           // 当前实际最佳拼接方案的纸片数。   
  158. int ncount[NCELL];      // 记录拼接方案中重复纸片的张数。   
  159. int cell[NCELL][NCELL];     // 尝试拼接时使用的网格。   
  160. int backup[NCELL][NCELL];   // 记录实际最佳方案网格状态。   
  161. int ncache[NMAX];       // 记录的未成功拼接的拆分方案个数。   
  162. int cache[NMAX][SMAX][NMAX];    // 记录未能成功拼接的拆分方案。   
  163.   
  164. bool found;         // 当前是否发现了非最优拆分方案的实际拼接方案。   
  165. bool finished;          // 提前结束回溯的标志。   
  166.   
  167. // 在网格 cell 中查找边长为 size 的空白区域的左上角坐标。   
  168. int find(int size, point points[PMAX])  
  169. {  
  170.     // 初始时,找到的坐标个数为 0。   
  171.     int npoints = 0;  
  172.   
  173.     for (int y = 0; y <= (n - size); y++)  
  174.         for (int x = 0; x <= (n - size); x++)  
  175.             // 找到了空白点。   
  176.             if (cell[y][x] == 0)  
  177.             {  
  178.                 // 查看此点是否存在边长为 size 的正方形空白区域。   
  179.                 bool empty = true;  
  180.                 for (int i = y; i < (y + size); i++)  
  181.                 {  
  182.                     for (int j = x; j < (x + size); j++)  
  183.                         if (cell[i][j] != 0)  
  184.                         {  
  185.                             empty = false;  
  186.                             break;  
  187.                         }  
  188.   
  189.                     if (!empty)  
  190.                         break;  
  191.                 }  
  192.   
  193.                 // 存在边长为 size 的空白区域,记录起点坐标。   
  194.                 if (empty)  
  195.                 {  
  196.                     points[npoints].x = x;  
  197.                     points[npoints].y = y;  
  198.   
  199.                     npoints++;  
  200.                 }  
  201.             }  
  202.   
  203.     return npoints;  
  204. }  
  205.   
  206. // 输出拼接方案。   
  207. void print(square s[NMAX], int nsquares)  
  208. {  
  209.   
  210. #ifdef DEBUG_MODE   
  211.     cout << "A FILL SOLUTION FOR SQUARE WITH SIZE: " << n << endl;  
  212.     for (int x = 0; x < n; x++)  
  213.     {  
  214.         for (int y = 0; y < n; y++)  
  215.             cout << (char) ('A' + backup[x][y] - 1) << " ";  
  216.         cout << endl;  
  217.     }  
  218. #endif   
  219.   
  220.     cout << nsquares << endl;  
  221.     for (int i = 0; i < nsquares; i++)  
  222.         cout << s[i].x << " " << s[i].y << " " << s[i].size << endl;  
  223. }  
  224.   
  225. // 尝试按照拆分方案 blocks 拼接边长为 n 的正方形。   
  226. void fill(int blocks[], int ncurrent, int goal, bool display_when_find)  
  227. {  
  228.     // 所有纸片均已匹配,表明该拆分方案可行,输出。   
  229.     if (ncurrent == goal)  
  230.     {  
  231.         memcpy(backup, cell, sizeof(cell));  
  232.   
  233.         // 是否显示结果。   
  234.         if (display_when_find)  
  235.             print(squares, ncurrent);  
  236.         else  
  237.             memcpy(best, squares, sizeof(squares));  
  238.   
  239.         finished = true;  
  240.     }  
  241.     else  
  242.     {  
  243.         int npoints;    // 记录找到的坐标个数。   
  244.         point points[PMAX]; // 记录起始坐标。   
  245.   
  246.         // 未找到则返回。   
  247.         if ((npoints = find(blocks[ncurrent], points)) == 0)  
  248.             return;  
  249.   
  250.         // 逐一尝试找到的位置。   
  251.         for (int i = 0; i < npoints; i++)  
  252.         {  
  253.             int x = points[i].x;  
  254.             int y = points[i].y;  
  255.             int s = blocks[ncurrent];  
  256.   
  257.             // 启发式规则:第一张纸片总是放置在左上角。   
  258.             if (ncurrent == 0 && (y != 0 || x != 0))  
  259.                 continue;  
  260.   
  261.             // 启发式规则:第二张纸片总是放置在右上角。   
  262.             if (ncurrent == 1 && (y != 0 || x != blocks[0]))  
  263.                 continue;  
  264.   
  265.             // 启发式规则:第三张纸片总是放置在左下角。   
  266.             if (ncurrent == 2 && (y != blocks[0] || x != 0))  
  267.                 continue;  
  268.   
  269.             // 启发式规则:第四张纸片总是靠近右侧边放置。   
  270.             if (ncurrent == 3 && x != (n - blocks[ncurrent]))  
  271.                 continue;  
  272.   
  273.             // 启发式规则:第五张纸片总是靠近右侧边或下边放置。   
  274.             if (ncurrent == 4 && (x != (n - blocks[ncurrent]) &&  
  275.                     y != (n - blocks[ncurrent])))  
  276.                 continue;  
  277.   
  278.             // 记录当前的拼接方法。注意坐标起点的不同,输出要求从起点 (1,1)    
  279.             // 开始输出。   
  280.             squares[ncurrent].x = (x + 1);  
  281.             squares[ncurrent].y = (y + 1);  
  282.             squares[ncurrent].size = s;  
  283.   
  284.             // 标记网格中的相应区域为填充状态。   
  285.             for (int gy = y; gy < (y + s); gy++)  
  286.                 for (int gx = x; gx < (x + s); gx++)  
  287.                     cell[gy][gx] = s;  
  288.   
  289.             // 继续向前匹配下一张纸片。   
  290.             fill(blocks, ncurrent + 1, goal, display_when_find);  
  291.   
  292.             // 是否结束回溯。   
  293.             if (finished)  
  294.                 return;  
  295.   
  296.             // 未结束回溯,表明当前拼接方案不可行,撤销对网格的更改。   
  297.             for (int gy = y; gy < (y + s); gy++)  
  298.                 for (int gx = x; gx < (x + s); gx++)  
  299.                     cell[gy][gx] = 0;  
  300.         }  
  301.     }  
  302. }  
  303.   
  304. // 排序函数的顺序规则。   
  305. bool cmp(int x, int y)  
  306. {  
  307.     return x > y;  
  308. }  
  309.   
  310. // 使用最少纸片数作为剪枝阈值搜索拆分方案。   
  311. void cut_by_tip(int area, int blocks[NMAX], int nblocks, int goal)  
  312. {  
  313.     // 当切分纸片数达到剪枝阈值,但仍有面积剩余,则结束回溯。   
  314.     if (area > 0 && nblocks == goal)  
  315.         return;  
  316.   
  317.     // 当切分完毕,切分的总纸片数不为剪枝阈值,则结束回溯。   
  318.     if (area == 0 && nblocks != goal)  
  319.         return;  
  320.   
  321.     // 切分完毕,且切分方案纸片张数为最少。   
  322.     if (area == 0)  
  323.     {  
  324.         int temp[NMAX];  
  325.   
  326.         // 注意数组作为形式参数时,传递的是指针,故不能使用 sizeof(blocks) 来计算   
  327.         // 数组 blocks 的大小。   
  328.         memcpy(temp, blocks, NMAX * sizeof(int));  
  329.   
  330.         // 将纸片大小按从大到小排序。   
  331.         sort(temp, temp + nblocks, cmp);  
  332.   
  333.         // 若在未成功拼接的方案中未找到当前切分方案,则尝试拼接。   
  334.         bool exist = false;  
  335.         for (int i = 0; i < ncache[nblocks - 1]; i++)  
  336.         {  
  337.             bool equal = true;  
  338.             for (int j = 0; j < nblocks; j++)  
  339.                 if (cache[nblocks - 1][i][j] != temp[j])  
  340.                 {  
  341.                     equal = false;  
  342.                     break;  
  343.                 }  
  344.   
  345.             if (equal)  
  346.             {  
  347.                 exist = true;  
  348.                 break;  
  349.             }  
  350.         }  
  351.   
  352.         // 不存在则尝试拼接。   
  353.         if (!exist)  
  354.         {  
  355.             // 重置网格。   
  356.             memset(cell, 0, sizeof(cell));  
  357.   
  358.             // 尝试拼接。   
  359.             fill(temp, 0, nblocks, true);  
  360.   
  361.             // 成功则返回。   
  362.             if (finished)  
  363.                 return;  
  364.   
  365.             // 拼接不成功,予以保存。   
  366.             memcpy(cache[nblocks - 1][ncache[nblocks - 1]++],  
  367.                 temp, sizeof(temp));  
  368.         }  
  369.   
  370.     }  
  371.     else  
  372.     {  
  373.         // 找到能切分出的最大边长的纸片。   
  374.         int up;  
  375.         for (int u = n - 1; u >= 1; u--)  
  376.             if (area >= (u * u))  
  377.             {  
  378.                 up = u;  
  379.                 break;  
  380.             }  
  381.   
  382.         // 启发式规则:优先考虑大小在 up / 2 + 1 和  up 之间的纸片。   
  383.         for (int r = (up / 2 + 1); r <= up; r++)  
  384.         {  
  385.             // 启发式规则:第二张纸片的大小与第一张纸片的大小和为 n。   
  386.             if (nblocks == 1 && (r + blocks[0]) != n)  
  387.                 continue;  
  388.   
  389.             // 启发式规则:第三张纸片的大小应该与第二张纸片的大小相同。   
  390.             if (nblocks == 2 && r != blocks[1])  
  391.                 continue;  
  392.   
  393.             // 启发式规则:第四张纸片和第五张纸片的大小之和应该为第一张纸片的大小,   
  394.             // 即拼接所得到的正方形某一边纸片数不能超过 3 张。   
  395.             if (nblocks == 4 && (r + blocks[3]) != blocks[0])  
  396.                 continue;  
  397.   
  398.             // 记录当前切分。   
  399.             blocks[nblocks] = r;  
  400.   
  401.             // 继续切分。   
  402.             cut_by_tip(area - (r * r), blocks, nblocks + 1, goal);  
  403.   
  404.             // 根据 finished 标志决定是否提前退出。   
  405.             if (finished)  
  406.                 return;  
  407.         }  
  408.     }  
  409. }  
  410.   
  411. // 使用回溯法构建拆分方案。参数为尚未切分的面积数量。   
  412. void cut_by_hard_work(int area, int blocks[NMAX], int nblocks)  
  413. {  
  414.     // 当切分纸片数达到当前可行的最小纸片数,但仍有面积剩余,不需继续尝试。   
  415.     if (area >= 0 && nblocks > smallest)  
  416.         return;  
  417.   
  418.     // 对于纸张数为 smallest 来说,已经找到了一个实际拼接方案,则对于同样的纸张数来说,其他   
  419.     // 拆分方案不必再去尝试。       
  420.     if (area == 0 && nblocks == smallest && found)  
  421.         return;  
  422.   
  423.     // 启发式规则:至少有两张边长为 1 的纸片。   
  424.     if (area == 0 && ncount[1] <= 1)  
  425.         return;  
  426.   
  427.     // 切分完毕,且切分方案纸片张数较当前最优值 smallest 小。   
  428.     if (area == 0)  
  429.     {  
  430.         int temp[NMAX];  
  431.   
  432.         // 注意数组作为形式参数时,传递的是指针,故不能使用 sizeof(blocks) 来计算   
  433.         // 数组 blocks 的大小。   
  434.         memcpy(temp, blocks, NMAX * sizeof(int));  
  435.   
  436.         // 将纸片大小按从大到小排序。   
  437.         sort(temp, temp + nblocks, cmp);  
  438.   
  439.         // 不检测当前方案是否与之前生成的未能成功拼接的方案重复,会增加搜索时间。   
  440.         // 但检测的话生成方案数很多,需要多量的内存。   
  441.           
  442.         // 重置网格。   
  443.         memset(cell, 0, sizeof(cell));  
  444.   
  445.         // 尝试拼接。   
  446.         finished = false;  
  447.         fill(temp, 0, nblocks, false);  
  448.         if (finished)  
  449.         {  
  450.             smallest = nblocks;  
  451.             found = true;  
  452.         }  
  453.     }  
  454.     else  
  455.     {  
  456.         // 找到能切分出的最大边长的纸片。   
  457.         int c, r, up, down, step;  
  458.         for (r = n - 2; r >= 1; r--)  
  459.             if (area >= (r * r))  
  460.                 break;  
  461.   
  462.         c = r;  
  463.         step = (nblocks == 0) ? 1 : (-1);  
  464.         r = (nblocks == 0) ? 1 : r;  
  465.   
  466.         for (; c >= 1; c--, r += step)  
  467.         {  
  468.             // 启发式规则:第一张纸片的大小在 n / 2 + 1 和  n - 2 之间的纸片。   
  469.             if (nblocks == 0 && r < (n / 2 + 1))  
  470.                 continue;  
  471.   
  472.             // 启发式规则:第二张纸片的大小与第一张纸片的大小和为 n。   
  473.             if (nblocks == 1 && (r + blocks[0]) != n)  
  474.                 continue;  
  475.   
  476.             // 启发式规则:第三张纸片的大小应该与第二张纸片的大小相同。   
  477.             if (nblocks == 2 && r != blocks[1])  
  478.                 continue;  
  479.   
  480.             // 启发式规则:第四张纸片和第五张纸片的大小之和应该为第一张纸片的大小,   
  481.             // 即拼接所得到的正方形某一边纸片数不能超过 3 张。   
  482.             if (nblocks == 4 && (r + blocks[3]) != blocks[0])  
  483.                 continue;  
  484.   
  485.             // 启发式规则:相同大小的纸片数不超过 4 张。   
  486.             if ((ncount[r] + 1) > 4)  
  487.                 continue;  
  488.   
  489.             ncount[r]++;  
  490.   
  491.             // 记录当前切分。   
  492.             blocks[nblocks] = r;  
  493.   
  494.             // 继续切分。   
  495.             cut_by_hard_work(area - (r * r), blocks, nblocks + 1);  
  496.   
  497.             ncount[r]--;  
  498.         }  
  499.     }  
  500. }  
  501.   
  502. // 使用已经生成好的最少纸片数拆分方案来得到拼接方案。   
  503. void solve_it_by_trick()  
  504. {  
  505.     // 查找相应质数的的拆分方案。   
  506.     int blocks[NMAX];  
  507.     int nblocks;  
  508.   
  509.     for (int r = 0; r < NPRIME; r++)  
  510.         if (trick[r][0] == n)  
  511.         {  
  512.             // 找到相应质数的数据,设置纸片总数及具体拆分方案。   
  513.             nblocks = trick[r][1];  
  514.   
  515.             for (int c = 0; c < nblocks; c++)  
  516.                 blocks[c] = trick[r][c + 2];  
  517.   
  518.             break;  
  519.         }  
  520.   
  521.     // 重置结束标志。   
  522.     finished = false;  
  523.   
  524.     // 重置网格数组。   
  525.     memset(cell, 0, sizeof(cell));  
  526.   
  527.     // 根据相应的最佳拆分方案拼接正方形。   
  528.     fill(blocks, 0, nblocks, true);  
  529. }  
  530.   
  531. // 使用最少纸片数拆分方案的纸片张数作为剪枝阈值,搜索可行的拼接方案。   
  532. void solve_it_by_tip()  
  533. {  
  534.     int blocks[NMAX];  
  535.   
  536.     finished = false;  
  537.   
  538.     memset(ncache, 0, sizeof(ncache));  
  539.   
  540.     // 若使用网络已经提供的拼接边长为 n 的正方形至少需要的纸张数,则可大   
  541.     // 大减少搜索时间,否则搜索时间很长。   
  542.     int goal = tip[n / 2 - 1];  
  543.   
  544.     // 用回溯法将 n * n 拆分为不大于 smallest 个平方数之和。   
  545.     cut_by_tip(n * n, blocks, 0, goal);  
  546. }  
  547.   
  548. // 利用求最大公约数的辗转相除法得到较好的拼接剪枝阈值。   
  549. void gcd(int a, int b)  
  550. {  
  551.     if (a < b)  
  552.     {  
  553.         int temp = a;  
  554.         a = b;  
  555.         b = temp;  
  556.     }  
  557.   
  558.     smallest += (a / b) * 2;  
  559.   
  560.     if (a % b != 0)  
  561.         gcd(a % b, b);  
  562. }  
  563.   
  564. // 完全靠实时回溯生成最少纸片数的拼接方案。   
  565. void solve_it_by_hard_work()  
  566. {  
  567.     int current[NMAX];  
  568.   
  569.     memset(ncache, 0, sizeof(ncache));  
  570.   
  571.     // 若使用网络已经提供的拼接边长为 n 的正方形至少需要的纸张数,则可大大减少搜索时间,否   
  572.     // 则搜索时间很长。若不使用,则在计算时需要动态调整 smallest 的值。当 n 逐渐增大时,可   
  573.     // 以选择较大的纸片来填充已减少剪枝阈值的值。   
  574.     smallest = 1 + n / 2 + (n - 2) / 2 + 4;  
  575.   
  576.     // 当 n 值较大时,试图找到一个更好的剪枝阈值。可以这样寻找:先放一张边长为 s 的纸片在左   
  577.     // 上角,然后再放一张边长为 (n - s) 的纸片在右下角,之后在剩余空间先填充边长为 (n -   
  578.     // s) 的纸片,剩余的空间则尽可能填充大的正方形,余下的填充边长为 1 的正方形纸片,这样   
  579.     // 获得的剪枝阈值较好。实际上可以利用求最大公约数的辗转相除法来得到。   
  580.     int threshold = smallest;  
  581.     for (int s = (n / 2 + 1); s < (n - 2); s++)  
  582.     {  
  583.         smallest = 2;  
  584.   
  585.         gcd(s, n - s);  
  586.   
  587.         if (threshold > smallest)  
  588.             threshold = smallest;  
  589.     }  
  590.   
  591.     smallest = threshold;  
  592.   
  593.     // 用回溯法将 n * n 拆分为不大于 smallest 个平方数之和。   
  594.     cut_by_hard_work(n * n, current, 0);  
  595.   
  596.     // 输出最佳方案。   
  597.     print(best, smallest);  
  598. }  
  599.   
  600. // 输出在坐标 (x,y) 边长为 size 的纸片。   
  601. void building(int x, int y, int size)  
  602. {  
  603.     cout << x << " " << y << " " << size << endl;  
  604. }  
  605.   
  606. int main(int ac, char *av[])  
  607. {  
  608.   
  609. #ifdef DEBUG_MODE   
  610.     clock_t start = clock();  
  611. #endif   
  612.   
  613.     int cases;      // 测试数据例数。   
  614.   
  615.     cin >> cases;  
  616.     while (cases--)  
  617.     {  
  618.         cin >> n;  
  619.   
  620.         // 若 n 为偶数,则直接输出拼接方案。   
  621.         if (n % 2 == 0)  
  622.         {  
  623.             int size = n / 2;  
  624.   
  625.             cout << "4" << endl;  
  626.   
  627.             building(1, 1, size);  
  628.             building(1, 1 + size, size);  
  629.             building(1 + size, 1, size);  
  630.             building(1 + size, 1 + size, size);  
  631.         }  
  632.         // 若 n 为形如 6 * m + 3 的数,则拼接方案与 n = 3 时相同,直接输出拼接方案。   
  633.         else if (n % 6 == 3)  
  634.         {  
  635.             int size = n / 3;  
  636.   
  637.             cout << "6" << endl;  
  638.   
  639.             building(1, 1, size * 2);  
  640.             building(1 + size * 2, 1, size);  
  641.             building(1 + size * 2, 1 + size, size);  
  642.             building(1 + size * 2, 1 + size * 2, size);  
  643.             building(1, 1 + size * 2, size);  
  644.             building(1 + size, 1 + size * 2, size);  
  645.         }  
  646.         // 对于 m 和 n,m 为质数且是能整除 n 的最小质数,则有 g(m) = g(n),且   
  647.         // 拼接方案类似。若 n 为奇数,且 5 是能整除 n 的最小质数,则 g(5) = g(n),   
  648.         // 包括 5 和 25。   
  649.         else if (n % 5 == 0)  
  650.         {  
  651.             int size = n / 5;  
  652.   
  653.             cout << "8" << endl;  
  654.   
  655.             building(1, 1, size * 3);  
  656.             building(1 + size * 3, 1, size * 2);  
  657.             building(1 + size * 3, 1 + size * 2, size * 2);  
  658.             building(1, 1 + size * 3, size * 2);  
  659.             building(1 + size * 2, 1 + size * 3, size);  
  660.             building(1 + size * 2, 1 + size * 4, size);  
  661.             building(1 + size * 3, 1 + size * 4, size);  
  662.             building(1 + size * 4, 1 + size * 4, size);  
  663.         }  
  664.         // 若 n 为奇数,且 7 是能整除 n 的最小质数,则 g(7) = g(n),包括 7 和 49。   
  665.         else if (n % 7 == 0)  
  666.         {  
  667.             int size = n / 7;  
  668.   
  669.             cout << "9" << endl;  
  670.   
  671.             building(1, 1, size * 4);  
  672.             building(1 + size * 4, 1, size * 3);  
  673.             building(1, 1 + size * 4, size * 3);  
  674.             building(1 + size * 3, 1 + size * 5, size * 2);  
  675.             building(1 + size * 5, 1 + size * 5, size * 2);  
  676.             building(1 + size * 5, 1 + size * 3, size * 2);  
  677.             building(1 + size * 4, 1 + size * 3, size);  
  678.             building(1 + size * 4, 1 + size * 4, size);  
  679.             building(1 + size * 3, 1 + size * 4, size);  
  680.         }  
  681.         // 对于其他情况,通过回溯找到满足题意的方案。   
  682.         else  
  683.         {  
  684.             // 使用已经生成好的拆分方案尝试拼接。使用此方法 UVa RT 为 0.032s。   
  685.             solve_it_by_trick();  
  686.   
  687.             // 直接使用最少纸片数作为剪枝阈值,使用回溯方法获得拆分方案,使用此   
  688.             // 方法得到 10 - 50 之间的所有质数的拆分方案运行时间为 27s。   
  689.             // solve_it_by_tip();   
  690.   
  691.             // 完全靠实时回溯获得拆分方案,然后尝试拼接,剪枝阈值使用非最优方案的   
  692.             // 纸片数。使用此方法在我的笔记本上 (Intel Core2 T5200 1.60GHz,   
  693.             // 2.0GiB 内存) 运行了半个多小时才得到 10 - 50 之间质数的拼接方案。   
  694.             // solve_it_by_hard_work();   
  695.         }  
  696.     }  
  697.   
  698. #ifdef DEBUG_MODE   
  699.     cout << "TIME ELAPSED: " << (clock() -  
  700.         start) / CLOCKS_PER_SEC << " s." << endl;  
  701. #endif   
  702.   
  703.     return 0;  
  704. }  
// Bigger Square Please... (拼接正方形)
// PC/UVa IDs: 110808/10270, Popularity: C, Success rate: high Level: 3
// Verdict: Accepted
// Submission Date: 2011-09-25
// UVa Run Time: 0.032s
//
// 版权所有(C)2011,邱秋。metaphysis # yeah dot net
//
// [问题描述]
// Tony 有很多张正方形纸片。这些纸片的边长为 1 到 N - 1 不等,且每种纸片都有无数张。但是他并不
// 满足。他想要一张更大的 ---- 边长为 N 的纸片。
//
// 可以把已有纸片拼接成他想要的大正方形。例如,一个边长为 7 的正方形可以通过如下 9 个更小的正方形
// 拼接而成(使用字母来填充相应的正方形,A 表示边长为 1 的正方形纸片,B 表示边长为 2 的正方形纸
// 片,依此类推):
//
//                B B B B C C C 
//                B B B B C C C 
//                A B B A C C C 
//                A B B D D D D 
//                C C C D D D D 
//                C C C D D D D 
//                C C C D D D D
//
// 在拼接出的正方形中间不能有空隙,不能有纸片超出正方形,且纸片不能相互重叠。并且,Tony 想要用尽
// 可能少的纸片来拼出这个大的正方形。你能帮助他吗?
//
// [输入]
// 输入第一行有一个单独的整数 T,表示测试数据的组数。每组数据为一个单独的整数 N(2 <= N <= 50)。
//
// [输出]
// 对于每组数据,输出一行,包含一个整数 K,表示最少需要的纸片数。接下来 K 行,每行三个整数 x,y,
// l,表示纸片左上角的坐标 (1 <= x,y <= N) 以及纸片的边长。
//
// [样例输入]
// 3
// 4
// 3
// 7
//
// [样例输出]
// 4
// 1 1 2
// 1 3 2
// 3 1 2
// 3 3 2
// 6
// 1 1 2
// 1 3 1
// 2 3 1
// 3 1 1
// 3 2 1
// 3 3 1
// 9
// 1 1 2
// 1 3 2
// 3 1 1
// 4 1 1
// 3 2 2
// 5 1 3
// 4 4 4
// 1 5 3
// 3 4 1
//
// [解题方法]
// 该题是 UVa 上的所有题目中 10% 较难的题目中的一题。如果是实时产生拼接方案,而不是预先生成拼接
// 方案再提交,说明水平确实比较高。题目要求用边长为 1 - (N - 1) 的任意张纸片拼接成一个边长为
// N 的正方形纸片,纸片间互相不能重叠,且不能超出边长为 N 的正方形范围,求使用纸片数最少的拼接方
// 案,以左上角为坐标起点 (1,1),按坐标、纸张边长的顺序输出每张纸片的坐标、边长值。
//
// 根据题意,设需要拼接的正方形边长为 N,很明显,拼接方案是一系列平方数的和。问题转化为如何将 N 表
// 示为平方数之和,且要求平方数的个数最小,这个问题可以通过回溯来解决。但是得到了一个将 N 拆分为平
// 方数之和的方案,并不表示就能将这些大小的纸片拼接成一个边长为 N 的纸片,例如,对于 N = 5,可以
// 拆分为 9 与 16 的和,9 和 16 都是平方数,但是实际上无法将一张边长为 3 和一张边长为 4 的纸片
// 拼接为一张边长为 5 的纸片,所以在生成一个平方数和方案后,需要实际尝试放置,若能放置,则表明此方
// 案可行,予以记录,将所有可行的方案记录后,挑选其中纸片数最小的方案即为所求。这样的话,将 N 拆分
// 为平方数之和是一步回溯,将拆分方案尝试放置又是一步回溯,需要通过两步回溯来解决本问题。
//
// 考虑到需要两步回溯,如果不予以充分剪枝,则计算时间将不可忍受,在 UVa BBS 上关于这个题目的讨论
// 即反映了这种情况。在将 N 拆分为平方数和的一步,若能生成一个拆分方案,尽管不是最优的,但是它的总
// 个数较小,且能实际放置,则将此方案的总个数作为剪枝阈值将可避免较多无效的搜索,将 N 拆分为平方数
// 后且能实际放置的一个非最优方案可以这样构造:左上角放置一个边长为 (N - 2) 的纸片,然后在右侧和
// 下方放置边长为 2 的纸片,剩余的空间放置边长为 1 的纸片,这样总的纸片需要数量为: 1 + [N / 2]
// + [(N - 2) / 2] + 4,其中符号 [] 表示取整,这样总的纸片放置数在 N 附近,可以做为一个较好
// 的剪枝阈值。通过回溯发现了总个数更少的方案后,则开始尝试实际放置,可以通过设立一个网格数组,当
// 填充边长为 A 的纸片时,在网格中查找是否有起始坐标为 (x,y) 且边长为 A 的空白区域,若无此种
// 空白区域,则表明该方案无法实际放置,若能找到,则找到所有这样的起始位置,逐一回溯进行尝试,尝试
// 某位置后,则将该区域标记为已填充,若后继填充不成功返回时则撤销标记。对于已经产生但不能实际拼接
// 的拆分方案需要予以记录,在后继生成的方案中,若有方案与记录的方案相同,则不必再次浪费时间搜索。
//
// 通过网络搜索该问题的相关信息可以知道,当 N MOD 2 = 0 或者 N MOD 6 = 3 时,有特殊的解法。
// 对于 N MOD 2 = 0,即 N 为偶数时,最少纸片数的拼接方法为:用 4 张边长为 N / 2 的纸片拼接得
// 到边长为 N 的正方形。当 N MOD 6 = 3 时,放置方法与 N = 3 的方案类似,只不过将相应的纸片边长
// 增加同样的数量。同时平方数如 25 的拼接方案和 5 的拼接方案类似,49 的拼接方案和 7 的拼接方案
// 类似,则在 2 - 50 之间的数,只需要求出质数的拼接方法即可。网络上已经有 2 - 50 之间的质数的
// 拼接方案和最少需要纸片数,利用这些信息,可以显著减少计算时间,甚至直接生成拼接方案后再提交。
//
// 参考网页:
// http://www2.stetson.edu/~efriedma/mathmagic/1298.html
// http://mathpuzzle.com/perkinsbestquilts.txt
// http://mathworld.wolfram.com/MrsPerkinssQuilt.html

#include <iostream>
#include <cstring>
#include <algorithm>
#include <ctime>

using namespace std;

#ifndef DEBUG_MODE
#define DEBUG_MODE	// 测试用,若在线提交,需要将该语句注释掉。
#endif

#define NMAX 20		// 拆分时,纸片最多需要的张数。
#define NPRIME 11	// 10 - 50 之间的质数个数。
#define SMAX 1024	// 最多保存的未成功拼接的拆分方案数。
#define PMAX 2500	// 网格中坐标最大个数。
#define NCELL 50	// 网格边长。

struct square		// 表示拼接的纸片信息。
{
	int x, y;	// 纸片在网格中的坐标。
	int size;	// 纸片的边长。
};

square squares[NMAX];	// 记录当前的拼接方案。
square best[NMAX];	// 记录搜索得到的最好实际拼接方案。

struct point	// 表示网格中的一个点。
{
	int x;		// 点的横坐标。
	int y;		// 点的纵坐标。
};

// 2 - 50 之间的奇数拼接时所需要的最少纸片数。
int tip[24] = { 6, 8, 9, 6, 11, 11, 6, 12, 13, 6, 13, 8, 6, 14, 15, 6, 8, 15, 6,
	15, 16, 6, 17, 9
};

// 10 - 50 之间的质数的最佳拆分方案。数组第一个数表示质数,第二个数表示所需纸片数,其后的数字为
// 纸片大小,按纸片从大到小排列。
int trick[NPRIME][NMAX] = {
	{11, 11, 6, 5, 5, 4, 2, 2, 2, 2, 1, 1, 1},				// 11
	{13, 11, 7, 6, 6, 4, 3, 3, 2, 2, 2, 1, 1},				// 13
	{17, 12, 9, 8, 8, 5, 4, 4, 3, 2, 2, 2, 1, 1},				// 17
	{19, 13, 10, 9, 9, 5, 5, 5, 3, 2, 2, 2, 1, 1, 1},			// 19
	{23, 13, 12, 11, 11, 7, 5, 5, 4, 3, 3, 2, 2, 1, 1},			// 23
	{29, 14, 17, 12, 12, 9, 8, 8, 4, 4, 3, 2, 2, 2, 1, 1},			// 29
	{31, 15, 16, 15, 15, 8, 8, 8, 4, 4, 4, 2, 2, 2, 1, 1, 1},		// 31
	{37, 15, 19, 18, 18, 11, 8, 8, 6, 5, 5, 3, 3, 2, 1, 1, 1},		// 37
	{41, 15, 23, 18, 18, 12, 11, 11, 7, 5, 4, 3, 3, 2, 2, 1, 1},		// 41
	{43, 16, 22, 21, 21, 11, 11, 11, 6, 5, 5, 3, 3, 3, 2, 1, 1, 1},		// 43
	{47, 17, 25, 22, 22, 13, 12, 9, 8, 8, 5, 5, 4, 3, 3, 2, 2, 1, 1},	// 47
};

int n;				// 要拼接的正方形边长。
int smallest;			// 当前实际最佳拼接方案的纸片数。
int ncount[NCELL];		// 记录拼接方案中重复纸片的张数。
int cell[NCELL][NCELL];		// 尝试拼接时使用的网格。
int backup[NCELL][NCELL];	// 记录实际最佳方案网格状态。
int ncache[NMAX];		// 记录的未成功拼接的拆分方案个数。
int cache[NMAX][SMAX][NMAX];	// 记录未能成功拼接的拆分方案。

bool found;			// 当前是否发现了非最优拆分方案的实际拼接方案。
bool finished;			// 提前结束回溯的标志。

// 在网格 cell 中查找边长为 size 的空白区域的左上角坐标。
int find(int size, point points[PMAX])
{
	// 初始时,找到的坐标个数为 0。
	int npoints = 0;

	for (int y = 0; y <= (n - size); y++)
		for (int x = 0; x <= (n - size); x++)
			// 找到了空白点。
			if (cell[y][x] == 0)
			{
				// 查看此点是否存在边长为 size 的正方形空白区域。
				bool empty = true;
				for (int i = y; i < (y + size); i++)
				{
					for (int j = x; j < (x + size); j++)
						if (cell[i][j] != 0)
						{
							empty = false;
							break;
						}

					if (!empty)
						break;
				}

				// 存在边长为 size 的空白区域,记录起点坐标。
				if (empty)
				{
					points[npoints].x = x;
					points[npoints].y = y;

					npoints++;
				}
			}

	return npoints;
}

// 输出拼接方案。
void print(square s[NMAX], int nsquares)
{

#ifdef DEBUG_MODE
	cout << "A FILL SOLUTION FOR SQUARE WITH SIZE: " << n << endl;
	for (int x = 0; x < n; x++)
	{
		for (int y = 0; y < n; y++)
			cout << (char) ('A' + backup[x][y] - 1) << " ";
		cout << endl;
	}
#endif

	cout << nsquares << endl;
	for (int i = 0; i < nsquares; i++)
		cout << s[i].x << " " << s[i].y << " " << s[i].size << endl;
}

// 尝试按照拆分方案 blocks 拼接边长为 n 的正方形。
void fill(int blocks[], int ncurrent, int goal, bool display_when_find)
{
	// 所有纸片均已匹配,表明该拆分方案可行,输出。
	if (ncurrent == goal)
	{
		memcpy(backup, cell, sizeof(cell));

		// 是否显示结果。
		if (display_when_find)
			print(squares, ncurrent);
		else
			memcpy(best, squares, sizeof(squares));

		finished = true;
	}
	else
	{
		int npoints;	// 记录找到的坐标个数。
		point points[PMAX];	// 记录起始坐标。

		// 未找到则返回。
		if ((npoints = find(blocks[ncurrent], points)) == 0)
			return;

		// 逐一尝试找到的位置。
		for (int i = 0; i < npoints; i++)
		{
			int x = points[i].x;
			int y = points[i].y;
			int s = blocks[ncurrent];

			// 启发式规则:第一张纸片总是放置在左上角。
			if (ncurrent == 0 && (y != 0 || x != 0))
				continue;

			// 启发式规则:第二张纸片总是放置在右上角。
			if (ncurrent == 1 && (y != 0 || x != blocks[0]))
				continue;

			// 启发式规则:第三张纸片总是放置在左下角。
			if (ncurrent == 2 && (y != blocks[0] || x != 0))
				continue;

			// 启发式规则:第四张纸片总是靠近右侧边放置。
			if (ncurrent == 3 && x != (n - blocks[ncurrent]))
				continue;

			// 启发式规则:第五张纸片总是靠近右侧边或下边放置。
			if (ncurrent == 4 && (x != (n - blocks[ncurrent]) &&
					y != (n - blocks[ncurrent])))
				continue;

			// 记录当前的拼接方法。注意坐标起点的不同,输出要求从起点 (1,1) 
			// 开始输出。
			squares[ncurrent].x = (x + 1);
			squares[ncurrent].y = (y + 1);
			squares[ncurrent].size = s;

			// 标记网格中的相应区域为填充状态。
			for (int gy = y; gy < (y + s); gy++)
				for (int gx = x; gx < (x + s); gx++)
					cell[gy][gx] = s;

			// 继续向前匹配下一张纸片。
			fill(blocks, ncurrent + 1, goal, display_when_find);

			// 是否结束回溯。
			if (finished)
				return;

			// 未结束回溯,表明当前拼接方案不可行,撤销对网格的更改。
			for (int gy = y; gy < (y + s); gy++)
				for (int gx = x; gx < (x + s); gx++)
					cell[gy][gx] = 0;
		}
	}
}

// 排序函数的顺序规则。
bool cmp(int x, int y)
{
	return x > y;
}

// 使用最少纸片数作为剪枝阈值搜索拆分方案。
void cut_by_tip(int area, int blocks[NMAX], int nblocks, int goal)
{
	// 当切分纸片数达到剪枝阈值,但仍有面积剩余,则结束回溯。
	if (area > 0 && nblocks == goal)
		return;

	// 当切分完毕,切分的总纸片数不为剪枝阈值,则结束回溯。
	if (area == 0 && nblocks != goal)
		return;

	// 切分完毕,且切分方案纸片张数为最少。
	if (area == 0)
	{
		int temp[NMAX];

		// 注意数组作为形式参数时,传递的是指针,故不能使用 sizeof(blocks) 来计算
		// 数组 blocks 的大小。
		memcpy(temp, blocks, NMAX * sizeof(int));

		// 将纸片大小按从大到小排序。
		sort(temp, temp + nblocks, cmp);

		// 若在未成功拼接的方案中未找到当前切分方案,则尝试拼接。
		bool exist = false;
		for (int i = 0; i < ncache[nblocks - 1]; i++)
		{
			bool equal = true;
			for (int j = 0; j < nblocks; j++)
				if (cache[nblocks - 1][i][j] != temp[j])
				{
					equal = false;
					break;
				}

			if (equal)
			{
				exist = true;
				break;
			}
		}

		// 不存在则尝试拼接。
		if (!exist)
		{
			// 重置网格。
			memset(cell, 0, sizeof(cell));

			// 尝试拼接。
			fill(temp, 0, nblocks, true);

			// 成功则返回。
			if (finished)
				return;

			// 拼接不成功,予以保存。
			memcpy(cache[nblocks - 1][ncache[nblocks - 1]++],
				temp, sizeof(temp));
		}

	}
	else
	{
		// 找到能切分出的最大边长的纸片。
		int up;
		for (int u = n - 1; u >= 1; u--)
			if (area >= (u * u))
			{
				up = u;
				break;
			}

		// 启发式规则:优先考虑大小在 up / 2 + 1 和  up 之间的纸片。
		for (int r = (up / 2 + 1); r <= up; r++)
		{
			// 启发式规则:第二张纸片的大小与第一张纸片的大小和为 n。
			if (nblocks == 1 && (r + blocks[0]) != n)
				continue;

			// 启发式规则:第三张纸片的大小应该与第二张纸片的大小相同。
			if (nblocks == 2 && r != blocks[1])
				continue;

			// 启发式规则:第四张纸片和第五张纸片的大小之和应该为第一张纸片的大小,
			// 即拼接所得到的正方形某一边纸片数不能超过 3 张。
			if (nblocks == 4 && (r + blocks[3]) != blocks[0])
				continue;

			// 记录当前切分。
			blocks[nblocks] = r;

			// 继续切分。
			cut_by_tip(area - (r * r), blocks, nblocks + 1, goal);

			// 根据 finished 标志决定是否提前退出。
			if (finished)
				return;
		}
	}
}

// 使用回溯法构建拆分方案。参数为尚未切分的面积数量。
void cut_by_hard_work(int area, int blocks[NMAX], int nblocks)
{
	// 当切分纸片数达到当前可行的最小纸片数,但仍有面积剩余,不需继续尝试。
	if (area >= 0 && nblocks > smallest)
		return;

	// 对于纸张数为 smallest 来说,已经找到了一个实际拼接方案,则对于同样的纸张数来说,其他
	// 拆分方案不必再去尝试。    
	if (area == 0 && nblocks == smallest && found)
		return;

	// 启发式规则:至少有两张边长为 1 的纸片。
	if (area == 0 && ncount[1] <= 1)
		return;

	// 切分完毕,且切分方案纸片张数较当前最优值 smallest 小。
	if (area == 0)
	{
		int temp[NMAX];

		// 注意数组作为形式参数时,传递的是指针,故不能使用 sizeof(blocks) 来计算
		// 数组 blocks 的大小。
		memcpy(temp, blocks, NMAX * sizeof(int));

		// 将纸片大小按从大到小排序。
		sort(temp, temp + nblocks, cmp);

		// 不检测当前方案是否与之前生成的未能成功拼接的方案重复,会增加搜索时间。
		// 但检测的话生成方案数很多,需要多量的内存。
		
		// 重置网格。
		memset(cell, 0, sizeof(cell));

		// 尝试拼接。
		finished = false;
		fill(temp, 0, nblocks, false);
		if (finished)
		{
			smallest = nblocks;
			found = true;
		}
	}
	else
	{
		// 找到能切分出的最大边长的纸片。
		int c, r, up, down, step;
		for (r = n - 2; r >= 1; r--)
			if (area >= (r * r))
				break;

		c = r;
		step = (nblocks == 0) ? 1 : (-1);
		r = (nblocks == 0) ? 1 : r;

		for (; c >= 1; c--, r += step)
		{
			// 启发式规则:第一张纸片的大小在 n / 2 + 1 和  n - 2 之间的纸片。
			if (nblocks == 0 && r < (n / 2 + 1))
				continue;

			// 启发式规则:第二张纸片的大小与第一张纸片的大小和为 n。
			if (nblocks == 1 && (r + blocks[0]) != n)
				continue;

			// 启发式规则:第三张纸片的大小应该与第二张纸片的大小相同。
			if (nblocks == 2 && r != blocks[1])
				continue;

			// 启发式规则:第四张纸片和第五张纸片的大小之和应该为第一张纸片的大小,
			// 即拼接所得到的正方形某一边纸片数不能超过 3 张。
			if (nblocks == 4 && (r + blocks[3]) != blocks[0])
				continue;

			// 启发式规则:相同大小的纸片数不超过 4 张。
			if ((ncount[r] + 1) > 4)
				continue;

			ncount[r]++;

			// 记录当前切分。
			blocks[nblocks] = r;

			// 继续切分。
			cut_by_hard_work(area - (r * r), blocks, nblocks + 1);

			ncount[r]--;
		}
	}
}

// 使用已经生成好的最少纸片数拆分方案来得到拼接方案。
void solve_it_by_trick()
{
	// 查找相应质数的的拆分方案。
	int blocks[NMAX];
	int nblocks;

	for (int r = 0; r < NPRIME; r++)
		if (trick[r][0] == n)
		{
			// 找到相应质数的数据,设置纸片总数及具体拆分方案。
			nblocks = trick[r][1];

			for (int c = 0; c < nblocks; c++)
				blocks[c] = trick[r][c + 2];

			break;
		}

	// 重置结束标志。
	finished = false;

	// 重置网格数组。
	memset(cell, 0, sizeof(cell));

	// 根据相应的最佳拆分方案拼接正方形。
	fill(blocks, 0, nblocks, true);
}

// 使用最少纸片数拆分方案的纸片张数作为剪枝阈值,搜索可行的拼接方案。
void solve_it_by_tip()
{
	int blocks[NMAX];

	finished = false;

	memset(ncache, 0, sizeof(ncache));

	// 若使用网络已经提供的拼接边长为 n 的正方形至少需要的纸张数,则可大
	// 大减少搜索时间,否则搜索时间很长。
	int goal = tip[n / 2 - 1];

	// 用回溯法将 n * n 拆分为不大于 smallest 个平方数之和。
	cut_by_tip(n * n, blocks, 0, goal);
}

// 利用求最大公约数的辗转相除法得到较好的拼接剪枝阈值。
void gcd(int a, int b)
{
	if (a < b)
	{
		int temp = a;
		a = b;
		b = temp;
	}

	smallest += (a / b) * 2;

	if (a % b != 0)
		gcd(a % b, b);
}

// 完全靠实时回溯生成最少纸片数的拼接方案。
void solve_it_by_hard_work()
{
	int current[NMAX];

	memset(ncache, 0, sizeof(ncache));

	// 若使用网络已经提供的拼接边长为 n 的正方形至少需要的纸张数,则可大大减少搜索时间,否
	// 则搜索时间很长。若不使用,则在计算时需要动态调整 smallest 的值。当 n 逐渐增大时,可
	// 以选择较大的纸片来填充已减少剪枝阈值的值。
	smallest = 1 + n / 2 + (n - 2) / 2 + 4;

	// 当 n 值较大时,试图找到一个更好的剪枝阈值。可以这样寻找:先放一张边长为 s 的纸片在左
	// 上角,然后再放一张边长为 (n - s) 的纸片在右下角,之后在剩余空间先填充边长为 (n -
	// s) 的纸片,剩余的空间则尽可能填充大的正方形,余下的填充边长为 1 的正方形纸片,这样
	// 获得的剪枝阈值较好。实际上可以利用求最大公约数的辗转相除法来得到。
	int threshold = smallest;
	for (int s = (n / 2 + 1); s < (n - 2); s++)
	{
		smallest = 2;

		gcd(s, n - s);

		if (threshold > smallest)
			threshold = smallest;
	}

	smallest = threshold;

	// 用回溯法将 n * n 拆分为不大于 smallest 个平方数之和。
	cut_by_hard_work(n * n, current, 0);

	// 输出最佳方案。
	print(best, smallest);
}

// 输出在坐标 (x,y) 边长为 size 的纸片。
void building(int x, int y, int size)
{
	cout << x << " " << y << " " << size << endl;
}

int main(int ac, char *av[])
{

#ifdef DEBUG_MODE
	clock_t start = clock();
#endif

	int cases;		// 测试数据例数。

	cin >> cases;
	while (cases--)
	{
		cin >> n;

		// 若 n 为偶数,则直接输出拼接方案。
		if (n % 2 == 0)
		{
			int size = n / 2;

			cout << "4" << endl;

			building(1, 1, size);
			building(1, 1 + size, size);
			building(1 + size, 1, size);
			building(1 + size, 1 + size, size);
		}
		// 若 n 为形如 6 * m + 3 的数,则拼接方案与 n = 3 时相同,直接输出拼接方案。
		else if (n % 6 == 3)
		{
			int size = n / 3;

			cout << "6" << endl;

			building(1, 1, size * 2);
			building(1 + size * 2, 1, size);
			building(1 + size * 2, 1 + size, size);
			building(1 + size * 2, 1 + size * 2, size);
			building(1, 1 + size * 2, size);
			building(1 + size, 1 + size * 2, size);
		}
		// 对于 m 和 n,m 为质数且是能整除 n 的最小质数,则有 g(m) = g(n),且
		// 拼接方案类似。若 n 为奇数,且 5 是能整除 n 的最小质数,则 g(5) = g(n),
		// 包括 5 和 25。
		else if (n % 5 == 0)
		{
			int size = n / 5;

			cout << "8" << endl;

			building(1, 1, size * 3);
			building(1 + size * 3, 1, size * 2);
			building(1 + size * 3, 1 + size * 2, size * 2);
			building(1, 1 + size * 3, size * 2);
			building(1 + size * 2, 1 + size * 3, size);
			building(1 + size * 2, 1 + size * 4, size);
			building(1 + size * 3, 1 + size * 4, size);
			building(1 + size * 4, 1 + size * 4, size);
		}
		// 若 n 为奇数,且 7 是能整除 n 的最小质数,则 g(7) = g(n),包括 7 和 49。
		else if (n % 7 == 0)
		{
			int size = n / 7;

			cout << "9" << endl;

			building(1, 1, size * 4);
			building(1 + size * 4, 1, size * 3);
			building(1, 1 + size * 4, size * 3);
			building(1 + size * 3, 1 + size * 5, size * 2);
			building(1 + size * 5, 1 + size * 5, size * 2);
			building(1 + size * 5, 1 + size * 3, size * 2);
			building(1 + size * 4, 1 + size * 3, size);
			building(1 + size * 4, 1 + size * 4, size);
			building(1 + size * 3, 1 + size * 4, size);
		}
		// 对于其他情况,通过回溯找到满足题意的方案。
		else
		{
			// 使用已经生成好的拆分方案尝试拼接。使用此方法 UVa RT 为 0.032s。
			solve_it_by_trick();

			// 直接使用最少纸片数作为剪枝阈值,使用回溯方法获得拆分方案,使用此
			// 方法得到 10 - 50 之间的所有质数的拆分方案运行时间为 27s。
			// solve_it_by_tip();

			// 完全靠实时回溯获得拆分方案,然后尝试拼接,剪枝阈值使用非最优方案的
			// 纸片数。使用此方法在我的笔记本上 (Intel Core2 T5200 1.60GHz,
			// 2.0GiB 内存) 运行了半个多小时才得到 10 - 50 之间质数的拼接方案。
			// solve_it_by_hard_work();
		}
	}

#ifdef DEBUG_MODE
	cout << "TIME ELAPSED: " << (clock() -
		start) / CLOCKS_PER_SEC << " s." << endl;
#endif

	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值