C# 算法(一)

算法规定了解决问题的具体步骤,即先做什么、再做什么、最后做什么

递归算法 求 n!

        //递归算法 求 n!
        //设计递归函数时,我们必须为它设置一个结束递归的“出口”,否则函数会一直调用自身(死循环),直至运行崩溃
        public static int GetFactorial(int n)
        {
            if (n == 0 || n == 1)
            {
                return 1;
            }
            return n * GetFactorial(n - 1);
        }

斐波那契数列

        //斐波那契数列
        /*公元 1202 年,意大利数学家莱昂纳多·斐波那契提出了具备以下特征的数列:
        前两个数的值分别为 0 、1 或者 1、1;
        从第 3 个数字开始,它的值是前两个数字的和;
        为了纪念他,人们将满足以上两个特征的数列称为斐波那契数列*/

        //如1 1 2 3 5 8 13 21 34......

        /// <summary>
        /// 斐波那契数列
        /// </summary>
        /// <param name="index">表示求数列中第 index 个位置上的数的值</param>
        /// <returns></returns>
        public static int GetFibonacci(int index)
        {
            /*index = 1  1
             *index = 2  1
             *index = 3  index(2)+index(1)
             *index = 4  index(3)+index(2)
             *index = 5  index(4)+index(3)
             */

            if (index == 1 || index == 2)
            {
                return 1;
            }
            return GetFibonacci(index - 1) + GetFibonacci(index - 2);
        }

        //普通方式实现斐波那契数列,连续输出长度为 n 的斐波那契数列
        public static int[] GetFibonacci2(int len)
        {
            int[] arrays = new int[len];
            if (len > 0)
            {
                for (int i = 0; i <= len - 1 && i < 2; i++)
                {
                    arrays[i] = 1;
                }
                for (int i = 2; i <= len - 1; i++)
                {
                    arrays[i] = arrays[i - 1] + arrays[i - 2];
                }
            }

            return arrays;

        }

分治算法实现“求数组中最大值”

        //分治算法实现“求数组中最大值”

        /*
         * 分治算法中,“分治”即“分而治之”的意思。
         * 分治算法解决问题的思路是:先将整个问题拆分成多个相互独立且数据量更少的小问题,
         * 通过逐一解决这些简单的小问题,最终找到解决整个问题的方案。
         */

        public static int GetMax(int[] arrays, int left, int right)
        {
            //1.如果数组不存在或长度为0
            if (arrays == null || arrays.Length == 0)
            {
                return -1;
            }

            //2.如果查找范围中仅有 2 个数字,则直接比较即可
            if (right - left <= 1)
            {
                if (arrays[left] >= arrays[right])
                {
                    return arrays[left];
                }
                return arrays[right];
            }

            //3.等量划分为两个区域
            int middle = (right - left) / 2 + left;
            int max_left = GetMax(arrays, left, middle);
            int max_right = GetMax(arrays, middle + 1, right);
            if (max_left >= max_right)
            {
                return max_left;
            }
            else
            {
                return max_right;
            }
        }

        //普通算法实现“求数组中最大值”
        public static int GetMax2(int[] arrays)
        {
            int max = arrays[0];
            foreach (var item in arrays)
            {
                if (item > max)
                {
                    max = item;
                }
            }
            return max;
        }

汉诺塔问题

        /*
         * 汉诺塔问题源自印度一个古老的传说,印度教的“创造之神”梵天创造世界时做了 3 根金刚石柱,
         * 其中的一根柱子上按照从小到大的顺序摞着 64 个黄金圆盘。
         * 梵天命令一个叫婆罗门的门徒将所有的圆盘移动到另一个柱子上,移动过程中必须遵守以下规则:
         * 每次只能移动柱子最顶端的一个圆盘;
         * 每个柱子上,小圆盘永远要位于大圆盘之上;
         */

        //柱1(起始柱)  柱2(目标柱)  柱3(辅助柱)

        //汉诺塔问题中,2个圆盘移动3次,3 个圆盘至少需要移动 7 次,移动 n 的圆盘至少需要操作 2^n-1 次。

        /*对于 n 个圆盘的汉诺塔问题,移动圆盘的过程是:
         * 将起始柱上的 n-1 个圆盘移动到辅助柱上;
         * 将起始柱上遗留的 1 个圆盘移动到目标柱上;
         * 将辅助柱上的所有圆盘移动到目标柱上。
         * 由此,n 个圆盘的汉诺塔问题就简化成了 n-1 个圆盘的汉诺塔问题。
         * 按照同样的思路,n-1 个圆盘的汉诺塔问题还可以继续简化,直至简化为移动 3 个甚至更少圆盘的汉诺塔问题。
         */

        // 统计移动次数
        public static int i = 1;
        /// <summary>
        /// 以移动 3 个圆盘为例,起始柱、目标柱、辅助柱分别用 A、B、C 表示
        /// hanoi(3, 'A', 'B', 'C');
        /// </summary>
        /// <param name="num">移动圆盘的数量</param>
        /// <param name="sou">起始柱</param>
        /// <param name="tar">目标柱</param>
        /// <param name="sux">辅助柱</param>
        public static void hanoi(int num, char sou, char tar, char sux)
        {
            if (num == 1)
            {
                Console.WriteLine(i + ": from " + sou + " to " + tar);
                i++;
            }
            else
            {
                //递归调用 hanoi() 函数,将 num-1 个圆盘从起始柱移动到辅助柱上
                hanoi(num - 1, sou, sux, tar);
                 将起始柱上剩余的最后一个大圆盘移动到目标柱上
                Console.WriteLine(i + ": from " + sou + " to " + tar);
                i++;
                 递归调用 hanoi() 函数,将辅助柱上的 num-1 圆盘移动到目标柱上
                hanoi(num - 1, sux, tar, sou);

            }
        }

贪心算法解决部分背包问题

        /*贪心算法
         * 每一步都力求最大限度地解决问题,每一步都选择的是当前最优的解决方案,这种解决问题的算法就是贪心算法。
         * 虽然贪心算法每一步都是最优的解决方案,但整个算法并不一定是最优的
         * 贪心算法注重的是每一步选择最优的解决方案(又称“局部最优解”),并不关心整个解决方案是否最优。
         * 部分背包问题是使用贪心算法解决的典型案例之一,此外它还经常和其它算法混合使用,例如克鲁斯卡尔算法、迪杰斯特拉算法等
         */

        /*背包问题
         * 在限定条件下,如何从众多物品中选出收益最高的几件物品,这样的问题就称为背包问题。
         * 根据不同的限定条件,背包问题还可以有更细致的划分:
         * 0-1 背包问题:每件物品都不可再分,要么整个装入背包,要么放弃,不允许出现类似“将物品的 1/3 装入背包”的情况;
         * 部分背包问题:每件物品是可再分的,即允许将某件物品的一部分(例如 1/3)放入背包;
         * 完全背包问题:挑选物品时,每件物品可以选择多个,也就是说不限物品的数量。
         * 多重背包问题:每件物品的数量是有严格规定的,比如物品 A 有 2 件,物品 B 有 3 件。
         */


        //部分背包问题,每件物品是可再分的
        //假设商店中有 3 种商品,它们各自的重量和收益是:
        //商品 1:重量 10 斤,收益 60 元; -- 6
        //商品 2:重量 20 斤,收益 100 元; -- 5
        //商品 3:重量 30 斤,收益 120 元。 -- 4

        //对于每件商品,顾客可以购买商品的一部分(可再分)。一个小偷想到商店行窃,他的背包最多只能装 50 斤的商品,如何选择才能获得最大的收益呢?

        //贪心算法解决此问题的思路是:计算每个商品的收益率(收益/重量),优先选择收益率最大的商品,直至所选商品的总重量达到 50 斤。
        /// <summary>
        /// 贪心算法解决部分背包问题
        /// </summary>
        /// <param name="w">记录各个商品的总重量</param>
        /// <param name="p">记录各个商品的总价值</param>
        /// <param name="result">记录各个商品装入背包的比例</param>
        /// <param name="W">背包的容量</param>

        public static void fractional_knapsack(float[] w, float[] p, float[] result, float W)
        {
            //根据收益率,重新对商品进行排序,从大到小
            sort(w, p);
            int i = 0;
            //从收益率最高的商品开始装入背包,直至背包装满为止
            while (W > 0)
            {
                float temp = W > w[i] ? w[i] : W;
                result[i] = temp / w[i];
                W -= temp;
                i++;
            }

        }
        //根据收益率,重新对商品进行排序
        public static void sort(float[] w, float[] p)
        {
            int length = w.Length;
            //用v[]存商品的收益率
            float[] v = new float[length];
            for (int i = 0; i < length; i++)
            {
                v[i] = p[i] / w[i];  //总价值/总重量
            }
            //根据 v 数组记录的各个商品收益率的大小,同时对 w 和 p 数组进行排序,从大到小
            for (int i = 0; i < length; i++)
            {
                for (int j = i + 1; j < length; j++)
                {
                    if (v[i] < v[j])
                    {
                        float temp = v[i]; //v[i] 与v[j]交换
                        v[i] = v[j];
                        v[j] = temp;
                        temp = w[i];//w[i] 与w[j]交换
                        w[i] = w[j];
                        w[j] = temp;
                        temp = p[i];//p[i] 与p[j]交换
                        p[i] = p[j];
                        p[j] = temp;
                    }

                }
            }
        }
        //调用fractional_knapsack方法
        public static void Getfractional_knapsack()
        {
            float W = 50;
            float[] w = { 10, 30, 20 }; //各个商品的重量
            float[] p = { 60, 100, 120 }; //各个商品的价值
            float[] result = { 0, 0, 0 };//统计背包中商品的总收益

            fractional_knapsack(w, p, result, W);

            float values = 0;//统计背包中商品的总收益

            //根据 result 数组中记录的数据,决定装入哪些商品
            for (int i = 0; i < w.Length; i++)
            {
                if (result[i] == 1)
                {
                    Console.WriteLine("总重量为" + w[i] + ",总价值为" + p[i] + "的商品全部装入");
                }
                else if (result[i] == 0)
                {
                    Console.WriteLine("总重量为" + w[i] + ",总价值为" + p[i] + "的商品不装");
                }
                else
                {
                    Console.WriteLine("总重量为" + w[i] + ",总价值为" + p[i] + "的商品装入" + result[i] * 100 + "%");
                    values += p[i] * result[i];
                }
            }
            Console.WriteLine("最终收获的商品价值为" + values);
        }

动态规划算法解决01背包问题

         /*动态规划算法
         * 动态规划算法解决问题的过程和分治算法类似,也是先将问题拆分成多个简单的小问题,通过逐一解决这些小问题找到整个问题的答案。
         * 不同之处在于,分治算法拆分出的小问题之间是相互独立的,
         * 而动态规划算法拆分出的小问题之间相互关联,例如要想解决问题 A,必须先解决问题 B 和 C。
         * 给大家举过一个例子,假设有 1、7、10 这 3 种面值的纸币,每种纸币使用的数量不限,要求用尽可能少的纸币拼凑出的总面值为 15。
         * 贪心算法最终给出的拼凑方案是需要 10+1+1+1+1+1 共 6 张纸币,
         * 而如果用动态规划算法解决这个问题,可以得出最优的拼凑方案,即只需要 1+7+7 共 3 张纸币。
         * 
         * 动态规划算法的解题思路是:用 f(n) 表示凑齐面值 n 所需纸币的最少数量,面值 15 的拼凑方案有 3 种,分别是:

         * f(15) = f(14) +1:挑选一张面值为 1 的纸币,f(14) 表示拼凑出面值 14 所需要的最少的纸币数量;
         * f(15) = f(8) + 1:挑选一张面值为 7 的纸币,f(8) 表示拼凑出面值 8 所需要的最少的纸币数量;
         * f(15) = f(5) + 1:选择一张面值为10 的纸币,f(5) 表示拼凑出面值 5 所需要的最少的纸币数量。
         * 
         * 也就是说,f(14)+1、f(8)+1 和 f(5)+1 三者中的最小值就是最优的拼凑方案。采用同样的方法,继续求 f(14)、f(8)、f(5) 的值:
         * f(5) = f(4) + 1;
         * f(8) = f(7) + 1 = f(1) +1;
         * f(14) = f(13)+1 = f(7) + 1 = f(4) +1。
         * 
         * 感兴趣的读者还可以继续拆分 f(4)、f(7)、f(13) 等。经过不断地拆分,f(15) 最终会和 f(0)、f(1)、f(2) 等产生关联,而 f(0)、f(1)、f(2) 的值是很容易计算的。在得知 f(0)、f(1)、f(2) 等简单问题的结果后,就可以轻松推算出 f(3)~f(14) 的值,最终可以推算出 f(15) 的值。
         * 
         * 背包问题的种类有很多,其中部分背包问题可以用贪心算法解决,而 0-1 背包问题则可以用动态规划算法解决
         */


        /* 01背包问题:所有物品不可再分,要么整个装入背包,要么放弃,不允许出现“仅选择物品的 1/3 装入背包”的情况;
        * 动态规划算法解决01背包问题
                	            
                商品种类\背包承重    0	1	2	3	4	5	6	7	8	9	10	11
                不装任何商品	        0	0	0	0	0	0	0	0	0	0	0	0
                w1 = 1,v1 = 1	    0	1	1	1	1	1	1	1	1	1	1	1
                w2 = 2,v2 = 6	    0	1	6	7	7	7	7	7	7	7	7	7
                w3 = 5,v3 = 18	    0	1	6	7	7	18	19	24	25	25	25	25
                w4 = 6,v4 = 22	    0	1	6	7	7	18	22	24	28	29	29	40
                w5 = 7,v5 = 28	    0	1	6	7	7	18	22	28	29	34	35	40
        */

        static int N = 5;//商品的种类
        static int W = 11;//背包的承重
        /// <summary>
        /// 动态规划算法解决01背包问题
        /// </summary>
        /// <param name="result">存储最终的结果</param>
        /// <param name="w">存储各商品的重量</param>
        /// <param name="v">存储各商品的价值</param>
        public static void knapsack01(int[,] result, int[] w, int[] v)
        {
            //逐个遍历每个商品
            for(int i = 1;i <= N; i++)  //行
            {
                //求出 1 - W各个承重对应的最大收益
                for(int j = 1; j <= W; j++)  //列
                {

                    if(j < w[i])  //背包承重小于商品总重量
                    {
                        result[i,j] = result[i - 1,j];
                    }
                    else
                    {
                        // 比较装入该商品和不装该商品,哪种情况获得的收益更大,记录最大收益值
                        result[i,j] = result[i - 1,j] > (v[i] + result[i - 1,j - w[i]]) ? result[i - 1,j] : (v[i] + result[i - 1,j - w[i]]);
                    }
                }
            }
        }

        追溯选中的商品
        public static void select(int[,] result,int[] w,int[] v)
        {
            int n = N;//商品的种类
            int bagw = W;//背包的承重
            //逐个商品进行判断
            while (n > 0)
            {
                //如果在指定承重下,该商品对应的收益和上一个商品对应的收益相同,则表明未选中
                if (result[n,bagw] == result[n - 1,bagw])
                {
                    n--;
                }
                else
                {
                    Console.WriteLine("(" + w[n] + "," + v[n] + ") ");
                    bagw = bagw - w[n];
                    n--;
                }
            }
        }
        //调用knapsack01方法
        public static void GetKnapsack01()
        {
            int[] w = { 0, 1, 2, 5, 6, 7 }; //商品的重量
            int[] v = { 0, 1, 6, 18, 22, 28 };  //商品的价值
            int[,] result = new int[N + 1,W+1];
            knapsack01(result, w, v);
            Console.WriteLine("背包可容纳重量为 " + W + ",最大收益为 " + result[N,W]);
            Console.WriteLine("选择了");
            select(result, w, v);
            //背包可容纳重量为 11,最大收益为 40
            //选择了(6, 22)(5, 18)
        }

回溯算法解决迷宫问题


        //回溯算法
        /*
        回溯算法和穷举法很像,它也会把所有可能的方案都查看一遍,从中找到正确答案。不同之处在于,回溯算法查看每种方案时,一旦判定其不是正确答案,会立即以“回溯”的方式试探其它方案。

        所谓“回溯”,其实就是回退、倒退的意思。回溯算法查找从 A 到 K 路线的过程是:
        从 A 出发,先选择 A-B 路线;继续从 B 出发,先选择 B-C 路线;到达 C 点后发现无路可选,表明当前路线无法达到 K 点,该算法会立刻回退到上一个节点,也就是 B 点;
        从 B 点出发,选择 B-D 路线,达到 D 点后发现无法到达 K 点,该算法再回退到 B 点;
        从 B 点出发已经没有新的线路可以选择,该算法再次回退到 A 点,选择新的 A-E 路线;
        继续以同样的方式测试 A-E-F-G、A-E-F-H、A-E-J-I 这 3 条线路后,最终找到 A-E-J-K 路线。
        
        回溯算法采用“回溯”(回退)的方式对所有的可行方案做出判断,并最终找到正确方案。和穷举法相比,回溯算法的查找效率往往更高,因为在已经断定当前方案不可行的情况下,回溯算法能够“悬崖勒马”,及时转向去判断其它的可行方案。
        
        回溯算法经常以递归的方式实现,用来解决以下 3 类问题:
        决策问题:从众多选择中找到一个可行的解决方案;
        优化问题:从众多选择中找到一个最佳的解决方案;
        枚举问题:找出能解决问题的所有方案。
        
        用回溯算法解决的经典问题有 N皇后问题、迷宫问题等
         */

        
        //回溯算法解决此问题的具体思路是:
        //从当前位置开始,分别判断是否可以向 4 个方向(上、下、左、右)移动:
        //选择一个方向并移动到下个位置。判断此位置是否为终点,如果是就表示找到了一条移动路线;如果不是,在当前位置继续判断是否可以向 4 个方向移动;
        //如果 4 个方向都无法移动,则回退至之前的位置,继续判断其它的方向;
        //重复 2、3 步,最终要么成功找到可行的路线,要么回退至起点位置,表明所有的路线都已经判断完毕。

        //程序中,我们可以用特殊的字符表示迷宫中的不同区域。例如,用 1 表示可以移动的白色区域,用 0 表示不能移动的黑色区域,迷宫可以用如下的 0-1 矩阵来表示:
        //1 0 1 1 1
        //1 1 1 0 1
        //1 0 0 1 1
        //1 0 0 1 0
        //1 0 0 1 1

        static bool find = false;
        static int ROW = 5;
        static int COL = 5;

        /// <summary>
        /// 回溯算法解决迷宫问题
        /// </summary>
        /// <param name="maze"></param>
        /// <param name="row"></param>
        /// <param name="col"></param>
        /// <param name="outrow"></param>
        /// <param name="outcol"></param>
        ///(row,col) 表示起点,(outrow,outcol)表示终点
        public static void maze_puzzle(char[,]maze,int row,int col,int outrow,int outcol)
        {
            maze[row, col] = 'Y'; //将各个走过的区域标记为 Y,起点
            if(row == outrow && col == outcol)
            {
                find = true;
                Console.WriteLine("成功走出迷宫,路线图为:");
                printmaze(maze);
                return;
            }
            //尝试向上移动
            if (canMove(maze, row - 1, col))
            {
                maze_puzzle(maze, row - 1, col, outrow, outcol);
                //如果程序不结束,表明此路不通,恢复该区域的标记
                maze[row - 1,col] = '1';
            }
            //尝试向左移动
            if (canMove(maze, row, col - 1))
            {
                maze_puzzle(maze, row, col - 1, outrow, outcol);
                //如果程序不结束,表明此路不通,恢复该区域的标记
                maze[row,col - 1] = '1';
            }
            //尝试向下移动
            if (canMove(maze, row + 1, col))
            {
                maze_puzzle(maze, row + 1, col, outrow, outcol);
                //如果程序不结束,表明此路不通,恢复该区域的标记
                maze[row + 1,col] = '1';
            }
            //尝试向右移动
            if (canMove(maze, row, col + 1))
            {
                maze_puzzle(maze, row, col + 1, outrow, outcol);
                //如果程序不结束,表明此路不通,恢复该区域的标记
                maze[row,col + 1] = '1';
            }
        }

        //判断(row,col)区域是否可以移动
        public static bool canMove(char[,] maze, int row, int col)
        {
            //如果目标区域位于地图内,不是黑色区域,且尚未移动过,返回 true:反之,返回 false
            return row >= 0 && row <= ROW - 1 && col >= 0 && col <= COL - 1 
                && maze[row,col] != '0' && maze[row,col] != 'Y';
        }
        public static void printmaze(char[,] maze)
        {
            for(int i = 0; i < ROW; i++)
            {
                for (int j= 0; j < COL; j++)
                {
                    Console.Write(maze[i, j]+"  ");
                }
                Console.Write("\r\n");
            }
        }
        public static void Getmaze_puzzle()
        {
            char[,] maze = new char[,]{
                {'1','0','1','1','1'},
                {'1','1','1','0','1'},
                {'1','0','0','1','1'},
                {'1','0','0','1','0'},
                {'1','0','0','1','1'} };
            maze_puzzle(maze, 0, 0, ROW - 1, COL - 1);
            if (find == false)
            {
                Console.WriteLine("未找到可行线路");
            }
            //Y  0  Y   Y   Y
            //Y  Y  Y   0   Y
            //1  0  0   Y   Y
            //1  0  0   Y   0
            //1  0  0   Y   Y

        }

posted @ 2021-12-21 13:43  highlightyys  阅读(29)  评论(0编辑  收藏  举报