首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

木块砌墙算法(C#源码)

  • 25-03-02 17:21
  • 2798
  • 10574
blog.csdn.net

木块砌墙算法(C#版)

首先感谢 网友“⒌1→⑤Ⅵ℡” 提供算法讲解和其他网友提供的帮助。

原题链接:http://hero.pongo.cn/Question/Details?ID=36&ExamID=36

题目镜像链接: http://iyenn.com/rec/1691917.html 

 

 

简化图形:因为厚度始终是1,所以这个墙可以简化成平面的图形。即 2^N * K 的图形。

 

    刚看到这个题,第一想到的就是遍历法,也想不出更好的办法。但是这个问题的规模很大,用遍历只能求规模很小的情况,当规模变大时,基本上不能用该算法进行求解。但是为了更快的看到结果,我还是实现了这个算法,这样可能有助于新算法的编写,至少可以知道规模较小情况下的结果,用于验证新算法是否正确。

 

本文使用的图示说明:

 

一、遍历方法求解

    我的遍历思路是:用二维数组模拟墙,从左下角开始摆放,从左往右,从下往上,最后一个格子是右上角那个位置;每个格子把每种可以摆放木块都摆放一次,每堆满一次算一种用摆放方法。下面图示演示算法过程(n=1,k=2)

为了便于描述,将为之进行编号。

下面是我的遍历算法的演示过程。

 

 

 

此方法优点,可以输出每种堆放方法。

因为这种算法只是过渡算法,所以这里不做详细解释。如有不理解的,请留言。下面给出上面算法的代码:

 

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. namespace HeapBlock
  5. {
  6. public delegate void OnFindWallDelegate(int[,] wall);
  7. public class Wall
  8. {
  9. private int width;
  10. private int height;
  11. private int[,] wall;
  12. private int x;
  13. private int y;
  14. private int result = 0;
  15. public Wall(int width, int height)
  16. {
  17. this.x = 0;
  18. this.y = 0;
  19. this.width = width;
  20. this.height = height;
  21. this.wall = new int[height, width];
  22. }
  23. public event OnFindWallDelegate OnFindWall;
  24. public int GetResult()
  25. {
  26. return result;
  27. }
  28. public void HeapBlock()
  29. {
  30. if (IsFull())
  31. {
  32. result++;
  33. if (OnFindWall != null)
  34. {
  35. OnFindWall(wall);
  36. }
  37. return;
  38. }
  39. if (CellNotUsed())
  40. {
  41. if (CanBeSetBlock(1))
  42. {
  43. this.SetBlock(1);
  44. this.NextCell();
  45. HeapBlock();
  46. this.PrevCell();
  47. this.ClearBlock(1);
  48. }
  49. if (CanBeSetBlock(2))
  50. {
  51. this.SetBlock(2);
  52. this.NextCell();
  53. HeapBlock();
  54. this.PrevCell();
  55. this.ClearBlock(2);
  56. }
  57. if (CanBeSetBlock(3))
  58. {
  59. this.SetBlock(3);
  60. this.NextCell();
  61. HeapBlock();
  62. this.PrevCell();
  63. this.ClearBlock(3);
  64. }
  65. }
  66. else
  67. {
  68. this.NextCell();
  69. HeapBlock();
  70. this.PrevCell();
  71. return;
  72. }
  73. }
  74. private void SetBlock(int blockType)
  75. {
  76. if (blockType == 1)
  77. {
  78. wall[y,x] = blockType;
  79. }
  80. else if (blockType == 2)
  81. {
  82. wall[y,x] = blockType;
  83. wall[y,x + 1] = -1;
  84. }
  85. else if (blockType == 3)
  86. {
  87. wall[y,x] = blockType;
  88. wall[y + 1,x] = -1;
  89. }
  90. }
  91. private void ClearBlock(int blockType)
  92. {
  93. if (blockType == 1)
  94. {
  95. wall[y,x] = 0;
  96. }
  97. else if (blockType == 2)
  98. {
  99. wall[y,x] = 0;
  100. wall[y,x + 1] = 0;
  101. }
  102. else if (blockType == 3)
  103. {
  104. wall[y,x] = 0;
  105. wall[y + 1,x] = 0;
  106. }
  107. }
  108. private bool CanBeSetBlock(int blockType)
  109. {
  110. if (!InWallArea() && !CellNotUsed())
  111. {
  112. return false;
  113. }
  114. if (blockType == 1)
  115. {
  116. return true;
  117. }
  118. else if (blockType == 2)
  119. {
  120. return InWallArea(x + 1, y) && CellNotUsed(x + 1, y);
  121. }
  122. else if (blockType == 3)
  123. {
  124. return InWallArea(x, y + 1) && CellNotUsed(x, y + 1);
  125. }
  126. else
  127. {
  128. return false;
  129. }
  130. }
  131. private bool CellNotUsed(int x, int y)
  132. {
  133. return wall[y,x] == 0;
  134. }
  135. private bool CellNotUsed()
  136. {
  137. return CellNotUsed(x, y);
  138. }
  139. private bool InWallArea(int x, int y)
  140. {
  141. return (0 <= x && x < width) && (0 <= y && y < height);
  142. }
  143. private bool InWallArea()
  144. {
  145. return InWallArea(x, y);
  146. }
  147. private void NextCell()
  148. {
  149. x = x < width - 1 ? x + 1 : 0;
  150. y = x == 0 ? y + 1 : y;
  151. }
  152. private void PrevCell()
  153. {
  154. x = x == 0 ? width - 1 : x - 1;
  155. y = x == width - 1 ? y - 1 : y;
  156. }
  157. private bool IsFull()
  158. {
  159. return x >= width || y >= height;
  160. }
  161. }
  162. }


 

 

Wall类使用方法:

  1. Wall wal = new Wall(width, height);
  2. //wal.OnFindWall += new OnFindWallDelegate(wal_OnFindWall); //可以注册事件。没找到一种堆放方法,就触发该事件。
  3. wal.HeapBlock(); //开始堆放木墙
  4. Console.Write(wal.GetResult()); //输出结果。


 

二、问题分解方法(递归)

问题分解就是把一个大问题,分解成小问题,逐个求解,然后再解决大问题。

首先说明“?方块”的作用

这个块表示右上角位置被半块绿色木块占用,其他位置空闲。

“?方块”,表示这个位置是空位置,可以任意摆放。

上图的意思就是,当右上角被绿色木块占用,此位置固定不变,其他位置任意摆放,在这种情况下的堆放方案数。

 

    假如有墙规模为(n,k),如果从中间切开,被分为规模问(n-1,k)的两堵墙。那么被分开的墙和原墙有什么关系呢?我们首先来看一下几组演示。

    首先演示,n=1,k=2时的算法演算

图 2-1

图示说明:

 表示,左边墙的所有堆放方案数 * 右边墙所有堆放方案数 = 2 * 2 = 4

表示,当切开处有一个横条的时候,空位置存在的堆放方案数。左边*右边 = 1*1 = 2;剩余两组以此类推。

这个是排列组合的知识。

 

    再演示一组更具一般性的计算分解。当n=2,k=3的情况。

 图 2-2

 

再从分解的结果中,挑选一组进行分解演示:

             

 图 2-3

通过图2-2和图2-3的分解演示,可以说明,最终都是分解成一列求解。在逐级向上汇总。

 

我们再假设一堵墙n=4,k=3。也就是说,宽度是16,高度是3。会有以下分解。

图2-4

 

根据上面的分解的一个中间结果,再进行分解,如下:

图 2-5

 

 

通过上面的演示可以明确一点,假设f(n)用于计算问题,那么f(n)依赖于f(n-1)的多种情况。我们再来看看,切开处的有什么特殊的地方。

通过对上面图示分解演示过程,可以知道,被切开的两堵墙从没有互相嵌入的木块(绿色木块)到全是互相连接的木块。切口绿色木块的全排列,有2^k种状,比如k=2,就有00、01、10、11,4中状态。根据排列组合的性质,把每一种状态下左右木墙堆放方案数相乘,再把所有乘积求和,就得到木墙的堆放结果数。以此类推,将问题逐步往下分解。从图2-5中可以看出,除了需要考虑切口绿色木块的状态,还需要考虑最左边一列和最右边一列的绿色木块状态。我们把这两种边界状态称为左边界状态和右边界状态,分别用leftState和rightState表示。 

 

观察图2-5被切分后,所有左边的墙,他们的左边界状态始终保持不变,右边界状态从0~maxState, maxState = 2^k-1(有绿色方块表示1,没有表示0;ls表示左边界状态,rs表示右边状态):

图 2-6

 同样可以看出右边的墙的右边界状态保持不变,而左边界状态从0~maxState。

要堆砌的木墙可以看做是左边界状态=0,和右边界状态=0的一堵墙。

 

根据图示分解过程可以得到以下方程

函数返回结果,就是,当左边状态为=leftState,右边状态=rightState时,木墙的堆砌方案数。本题要求解的就是左右状态都为0的情况。

如,n=1024,k=4,进行如下调用即可:f(1024,4,0,0)

 用方程来表示图形:

 

这个用方程表示为 f(2,3,2,5)

 

来看看方程是如何表示图形的分解。

比如,图2-2 的方程式表示为:

f(2,3,0,0) = f(1,3,0,0)*f(1,3,0,0)  + f(1,3,0,1)*f(1,3,1,0)  +  f(1,3,0,2)*f(1,3,2,0)
             + f(1,3,0,3)*f(1,3,3,0)  + f(1,3,0,4)*f(1,3,4,0)  +  f(1,3,0,5)*f(1,3,5,0)
            
+ f(1,3,0,6)*f(1,3,6,0)  + f(1,3,0,7)*f(1,3,7,0)

 

当n=0,表示墙就一列,此时不能再分解。直接计算结果进行返回。

当n=1,墙两列,分解时有些状态可能不能使用,需要排除。如图2-3

 

下面代码就是根据上面函数原理编写的。最终执行效率,n=1024,k=4 时,用时0.2800160秒。

源代码使用过了字典作为缓存,用时在1.3秒左右。后来改为数组结果,性能大增。

 

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.Collections;
  5. namespace HeapBlock
  6. {
  7. public class WoolWall
  8. {
  9. private int n;
  10. private int height;
  11. private int maxState;
  12. private int[, ,] resultCache; //结果缓存数组
  13. public WoolWall(int n, int height)
  14. {
  15. this.n = n;
  16. this.height = height;
  17. maxState = (1 << height) - 1;
  18. resultCache = new int[n + 1, maxState + 1, maxState + 1]; //构建缓存数组,每个值默认为0;
  19. }
  20. ///
  21. /// 静态入口。计算堆放方案数。
  22. ///
  23. ///
  24. ///
  25. ///
  26. public static int Heap(int n, int k)
  27. {
  28. return new WoolWall(n, k).Heap();
  29. }
  30. ///
  31. /// 计算堆放方案数。
  32. ///
  33. ///
  34. public int Heap()
  35. {
  36. return (int)Heap(n, 0, 0);
  37. }
  38. private long Heap(int n, int lState, int rState)
  39. {
  40. //如果缓存数组中的值不为0,则表示该结果已经存在缓存中。
  41. //直接返回缓存结果。
  42. if (resultCache[n, lState, rState] != 0)
  43. {
  44. return resultCache[n, lState, rState];
  45. }
  46. //在只有一列的情况,无法再进行切分
  47. //根据列状态计算一列的堆放方案
  48. if (n == 0)
  49. {
  50. return CalcOneColumnHeapCount(lState);
  51. }
  52. long result = 0;
  53. for (int state = 0; state <= maxState; state++)
  54. {
  55. if (n == 1)
  56. {
  57. //在只有两列的情况,判断当前状态在切分之后是否有效
  58. if (!StateIsAvailable(n, lState, rState, state))
  59. {
  60. continue;
  61. }
  62. result += Heap(n - 1, state | lState, state | lState) //合并状态。因为只有一列,所以lState和rState相同。
  63. * Heap(n - 1, state | rState, state | rState);
  64. }
  65. else
  66. {
  67. result += Heap(n - 1, lState, state) * Heap(n - 1, state, rState);
  68. }
  69. result %= 1000000007;//为了防止结果溢出,根据题目要求求模。
  70. }
  71. resultCache[n, lState, rState] = (int)result; //将结果写入缓存数组中
  72. resultCache[n, rState, lState] = (int)result; //对称的墙结果相同,所以直接写入缓存。
  73. return result;
  74. }
  75. ///
  76. /// 根据一列的状态,计算列的堆放方案数。
  77. ///
  78. /// 状态
  79. ///
  80. private int CalcOneColumnHeapCount(int state)
  81. {
  82. int sn = 0; //连续计数
  83. int result = 1;
  84. for (int i = 0; i < height; i++)
  85. {
  86. if ((state & 1) == 0)
  87. {
  88. sn++;
  89. }
  90. else
  91. {
  92. if (sn > 0)
  93. {
  94. result *= CalcAllState(sn);
  95. }
  96. sn = 0;
  97. }
  98. state >>= 1;
  99. }
  100. if (sn > 0)
  101. {
  102. result *= CalcAllState(sn);
  103. }
  104. return result;
  105. }
  106. ///
  107. /// 类似于斐波那契序列。
  108. /// f(1)=1
  109. /// f(2)=2
  110. /// f(n) = f(n-1)*f(n-2);
  111. /// 只是初始值不同。
  112. ///
  113. ///
  114. ///
  115. private static int CalcAllState(int k)
  116. {
  117. return k <= 2 ? k : CalcAllState(k - 1) + CalcAllState(k - 2);
  118. }
  119. ///
  120. /// 判断状态是否可用。
  121. /// 当n=1时,分割之后,左墙和右边墙只有一列。
  122. /// 所以state的状态码可能会覆盖原来的边缘状态。
  123. /// 如果有覆盖,则该状态不可用;没有覆盖则可用。
  124. /// 当n>1时,不存在这种情况,都返回状态可用。
  125. ///
  126. ///
  127. /// 左边界状态
  128. /// 右边界状态
  129. /// 切开位置的当前状态
  130. /// 状态有效返回 true,状态不可用返回 false
  131. private bool StateIsAvailable(int n, int lState, int rState, int state)
  132. {
  133. return (n > 1) || ((lState | state) == lState + state && (rState | state) == rState + state);
  134. }
  135. }
  136. }


 

代码分析

 

1,如何使用

    WoolWall.Heap(1024,4); //直接通过静态方法获得结果。

    new  WoolWall(n, k).Heap();//通过构造对象获得结果。

 

2,缓存

    因为他最终都是分解成一列的情况进行处理,这就会导致很慢。为了提高速度,本文使用了缓存机制来提高性能。缓存原理就是,n,k,leftState,rightState相同的墙,返回的结果肯定相同。利用这个特性,每计算一种结果就放入到缓存中,如果下次计算直接从缓存取出。刚开始缓存用字典类实现,有网友给出了更好的缓存方法——数组。这样性能好了很多,也更加简单。

 

3,核心算法讲解

上图反应了Heep调用的主要方法调用,在循环中,result 累加 lResult 和 rResult。 

在实际代码中,首先是从缓存中读取结果,如果没有缓存中读取结果在进行计算。 

分解法分解到一列时,不在分解,直接计算机过

  1. if (n == 0)
  2. {
  3. return CalcOneColumnHeapCount(lState);
  4. }

 

整个程序的核心代码

  1. for (int state = 0; state <= maxState; state++)
  2. {
  3. if (n == 1)
  4. {
  5. if (!StateIsAvailable(n, lState, rState, state))
  6. {
  7. continue;
  8. }
  9. result += Heap(n - 1, state | lState, state | lState) *
  10. Heap(n - 1, state | rState, state | rState);
  11. }
  12. else
  13. {
  14. result += Heap(n - 1, lState, state)
  15. * Heap(n - 1, state, rState);
  16. }
  17. result %= 1000000007;
  18. }


 

通过for循环,求和state=0到state=2^k-1的两边木墙乘积。

当n=1切分时,需要特殊考虑。如下图:

图2-7

看上图中,因为左边墙中间被绿色方块占用,所以在(1,0)-(1,1)这个位置不能再放绿色方块。所以一些状态需要排除,如state=2需要排除。同时在还需要合并状态,如state=1时,左边墙的状态=3。

 

CalcOneColumnHeap(int state)函数用于计算一列时摆放方案数。

计算方法是, 求和被绿色木块分割开的每一段连续方格的摆放方案数。每一段连续的方格的摆放方案通过CalcAllState方法求得。经过分析,可以得知CalcAllState是类似斐波那契序列的函数。

如:state = 4546(当然题目中state最大=15,这里只是为了演示如何计算),二进制是:1000111000010。位置上为1,表示被绿色木块占用,0表示空着,可以自由摆放。

1000111000010  被分割后 1  000  111  0000  1  0, 那么就有 000=3个连续位置, 0000=4个连续位置 , 0=1个连续位置,。堆放结果=CalcAllState(3) + CalcAllState(4) + CalcAllState(1) = 3 + 5 + 1 = 9;

 

三、优化算法。正向推导,减少调用次数

 

上面的性能不是很高,是因为调用性能的树形结构,形成了大量的函数调用和缓存查找, 为了得到更高的性能,可以通过新的解决思路。那就是所有的运算直接依赖于上一次运算的结果,这样可以防止更多的调用。如果每次运算都算出所有边界状态的结果,那么就能为下一次运算提供足够的信息。

下面是根这种思想编写的代码,

1024,4执行时间70ms左右以内

 

 

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. namespace HeapBlock
  5. {
  6. public class HeapTheWall
  7. {
  8. private int n;
  9. private int height;
  10. private int maxState;
  11. public HeapTheWall(int n, int height)
  12. {
  13. this.n = n;
  14. this.height = height;
  15. maxState = (1 << height) - 1;
  16. }
  17. public static int Heap(int n, int k)
  18. {
  19. return new HeapTheWall(n, k).Heap();
  20. }
  21. public int Heap()
  22. {
  23. int[,] result = new int[maxState + 1, maxState + 1];
  24. int[,] prevResult = new int[maxState + 1, maxState + 1];
  25. Heap(n, result, prevResult);
  26. return result[0, 0];
  27. }
  28. private void Heap(int n, int[,] result, int[,] prevResult)
  29. {
  30. if (n == 0)
  31. {
  32. CalcOneColumnResult(result);
  33. return;
  34. }
  35. Heap(n - 1, prevResult, result);
  36. for (int lState = 0; lState <= maxState; lState++)
  37. {
  38. for (int rState = 0; rState <= lState; rState++)
  39. {
  40. CalcResult(prevResult, result, lState, rState);
  41. result[rState, lState] = result[lState, rState];
  42. }
  43. }
  44. }
  45. private void CalcOneColumnResult(int[,] result)
  46. {
  47. for (int i = 0; i <= maxState; i++)
  48. {
  49. for (int j = 0; j <= maxState; j++)
  50. {
  51. result[i, j] = ((i | j) != i + j) ? 0 : CalcOneColumnHeap(i + j);
  52. }
  53. }
  54. }
  55. private void CalcResult(int[,] prevResult, int[,] result, int lState, int rState)
  56. {
  57. result[lState, rState] = 0;
  58. for (int middState = 0; middState <= maxState; middState++)
  59. {
  60. result[lState, rState] = (int)((result[lState, rState] + 1L * prevResult[lState, middState] * prevResult[middState, rState]) % 1000000007);
  61. }
  62. }
  63. private int CalcOneColumnHeap(int state)
  64. {
  65. int result = 1;
  66. foreach (int serialCount in GetAllSerialFreeCellCount(state))
  67. {
  68. result *= CalcAllState(serialCount);
  69. }
  70. return result;
  71. }
  72. private List<int> GetAllSerialFreeCellCount(int state)
  73. {
  74. List<int> result = new List<int>();
  75. int serialCountn = 0;
  76. for (int idx = 0; idx < height; idx++)
  77. {
  78. if ((state & 1) == 0)
  79. {
  80. serialCountn++;
  81. }
  82. else
  83. {
  84. if (serialCountn > 0)
  85. {
  86. result.Add(serialCountn);
  87. }
  88. serialCountn = 0;
  89. }
  90. state >>= 1;
  91. if (idx == height - 1)
  92. {
  93. if (serialCountn > 0)
  94. {
  95. result.Add(serialCountn);
  96. }
  97. }
  98. }
  99. return result;
  100. }
  101. private static int CalcAllState(int n)
  102. {
  103. return n <= 2 ? n : CalcAllState(n - 1) + CalcAllState(n - 2);
  104. }
  105. }
  106. }


 

1,如何调用?

     HeapTheWall.Heap(100,4);//直接通过静态方法调用

     new HeapTheWall(100,4).Heap();//通过构造对象,然后调用实例方法

 

2,思路

    创建两个数组,一个用于存放当前计算的,一个用于存放上一次计算的结果。这个数组除了包含墙的堆放结果(这其实就是边界状态为0,0是的堆放结果),还包括所有不同边界状态时的堆放结果。

    如果f(n,k)要直接依赖于f(n-1,k)的结果,那么f(n,k)每次计算就需要计算出左右边界状态所有组合情况的堆放数。

    此思路,首先计算出n=0时的结果,然后计算n=1时的结果,以此类推。

 

3,代码解释

        public int Heap()
        {
            int[,] result = new int[maxState + 1, maxState + 1];
            int[,] prevResult = new int[maxState + 1, maxState + 1];
            Heap(n, result, prevResult);
            return result[0, 0];
        }

       构造两个数组,一个用于存放零时结果(上一次的结果),一个用于存放最终计算结果。result[0,0] 存放状态(0,0) 的堆放方案数,result[lState,rState],返回状态(lState,rState)时墙的堆放方案数。

 

        private void Heap(int n, int[,] result, int[,] prevResult)
        {
            if (n == 0)
            {
                CalcOneColumnResult(result);
                return;
            }

            Heap(n - 1, prevResult, result);
            for (int lState = 0; lState <= maxState; lState++)
            {
                for (int rState = 0; rState <= lState; rState++)
                {
                    CalcResult(prevResult, result, lState, rState);
                    result[rState, lState] = result[lState, rState];
                }
            }
        }

     如果n == 0,直接计算出结果,如果n > 1 是,首先计算出 n-1 的结果。两个for循环则是遍历左右边界的所有状态组合,然后调用CalcResult方法,计算每种边界状态的结果,然后写入到结果数组中。

文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览49583 人正在系统学习中
注:本文转载自blog.csdn.net的红色标记的文章"http://blog.csdn.net/dw14132124/article/details/9038417"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top