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
}