一、入坑
1.何谓算法?
个人认为就是求解问题的策略。
2.为什么要学?
比如你面对一百万本无序书,你会如何去使他们有序呢?
自己一本一本编号整理?
比如,你要去出远门,你如何选择哪条路线,如何花钱最划算?
路程最短,花钱最少,怎么做呢?
比如,你面前有一百个人,如何找一个名字是“小明”的人?
直接大喊一声,谁是小明?或者从第一个开始挨个问,直到某个人回答自己是小明?
比如,人类基因工程取得重大进展,任务让识别人类DNA中所有的10万个基因,确定构成人类DNA的30亿个化学基对的序列。
该如何去做呢?
在一定前提下,你说交给计算机去干吧。如果计算机计算速度无限快,计算机存储器免费,或许不用怎么研究算法了。
但是计算机或许快,但不是无限,存储器或许是廉价的但不是免费的。所以高效的算法是很有必要的。
二、基本算法总结
1.枚举
它常用于解决是否存在或有多少可能的方法,基本思想是逐一列举问题的所有情形,并挨个检验。
常用的枚举有两种
递增枚举
输出1到100能被3整除的数字
#include<stdio.h>
#include<math.h>
void main(){
int i=1;
for(i;i<=100;i++){
if(i%3==0)
printf("%d \t",i);
}
}
区间枚举
统计由1,2,3,4,5,6,7,8,9全排列的9!个位数中平方数的个数,并输出最大的平方数
#include<stdio.h>
#include<math.h>
void main(){
int k,m,n,t,f[10];
long a,b,c,d,w;
n=0;
for(d=123456789;d<=987654321;d++){
a=(int)sqrt(d);
if(a*a!=d) continue; //确保d为平方数
for(k=0;k<=9;k++) f[k]=0;
w=d;
while(w>0)
{
m=w%10;
f[m]++;
w=w/10;
}
for(t=0,k=1;k<=9;k++)
if(f[k]!=1){ //测试平方数是否有重复数字
t=1;break;
}
if(t==0){
n++;b=a;c=d;
}
}
printf(" 排列中共有%d个平方数。\n",n);
printf(" 其中最大者为%ld=%ld^2 。\n",c,b);
}
枚举的一般步骤:
- 确定枚举策略,根据枚举路线设置枚举变量
- 确定具体范围,设计枚举循环
- 确定约束条件
- 设计程序并运行
2.递推
递推是利用问题本身具有的递推关系来求解问题,
运用递推通常需要求出递推关系式。
实施递推的一般步骤:
- 确定递推变量
- 建立递推关系式
- 确定初始(边界)条件
- 递推过程的控制
递推法求斐波那契数列
#include<stdio.h>
long getNumber(int n)
{
long result;
if(n==1||n==2)
{
result=n;
return result;
}
else
{
return getNumber(n-1)+getNumber(n-2);
}
}
int main()
{
int n,i,arr[100];
printf("请输入菲波那切数列的项数:");
scanf("%d",&n);
for(i=1;i<=n;i++)
{
printf("%ld\t",getNumber(i));
if(i%5==0)
printf("\n");
}
printf("\n");
}
迭代法求解斐波那契数列
#include<stdio.h>
#include<math.h>
void main(){
int k,n=10;
long a,b,c,s;
a=1;b=1;s=a+b; //迭代变量赋初值
for(k=3;k<=n;k++){ //控制迭代次数
a=a+b; //退出a是f数列的第k项
s=s+a; //推出s是f数列的前k项和
//a,b交换,为下次迭代作准备
c=b;
b=a;
a=c;
}
printf(" 前%ld项之和%ld \n",b,s);
}
提到递推就会说到迭代,迭代是一种不断用变量的旧值推出新值的变量,这个变量就是迭代变量。
递推也是根据递推关系式不断推出新值的过程。不严格的说,递推的实质就是迭代。
3.递归
递归通过函数或者过程调用自身将问题转化为本质相同但规模较小 的子问题,分治策略的具体体现。
递归方法容易描述,直观简单。
构建递归的基本步骤:
- 构建递归关系
- 确定递归边界
- 写出递归函数
- 设计主函数调用递归函数
辗转相除求最大公约数
#include<stdio.h>
long gcd(long a,long b){
return b==0?a:gcd(b,a%b);
}
void main(){
printf("%d",gcd(468595491,217906595));
}
递归求阶乘
#include<stdio.h>
long f(int x){
long g;
if(x==1||x==0)
g=1;
else
g=x*f(x-1);
return (g);
}
void main()
{
int n=10;
long y;
printf(" 计算n!,");
printf( "%d!=%ld\n",n,f(n));
}
求解汉诺塔
#include<stdio.h>
int k=0;
void move(char x,char y){ //输出函数
printf("%c-->%c ",x,y);
k++; //统计移动次数
if(k%5==0) printf("\n");
}
void hn(int m,char a,char b,char c){ //定义递归函数
if(m==1) move(a,c);
else //实施三步调用
{
hn(m-1,a,c,b);
move(a,c);
hn(m-1,b,a,c);
}
}
void main(){
int n;
printf(" 请输入盘的个数n: ");
scanf("%d",&n);
hn(n,'A','B','C');
printf("\n 共移动%d次 \n",k); //输出移动次数
}
在计数问题中,递归和递推可相互替代。
在构造性或者展示性问题,只有递归可以。
通常递归的效率低于递推。存在数据大量的进栈出栈和大量重复计算。
4.回溯法
按选优条件向前搜索,以达到目标。回溯法采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
迭代回溯描述
输入正整数n,m(n>=m)
i=1;a[i]=<赋初值>
while(){
for(g=1,k=i-1;k>=1;k--)
if(约束条件1) g=0
if(g&&<约束条件2>)
printf(a[1:m]);
if(i<n &&g)
{i++; a[i]=<取点值>; continue;}
}
while(a[i]==<回溯点>&&i>1) i--; //向前回溯
if(a[i]==n&&i==1) break; //退出循环结束
else a[i]=a[i]+1;
递归回溯描述
int put(int k)
{
int i,j,u;
if(k<=<规模>)
{
u=0;
if(约束条件1) u=1; //当u=1,不可操作
if(u==0){ //当u=1可操作
if(k== <规模>) //若满足规模,则打印出一个解
printf(一个解);
else put(k+1); //调用put(k+1)
}
}
}
问题:把前n个正整数围城一个环,如果环中所有相邻的两个数之和都是一个素数,该环称为一个n项素数和环。
对于指定的n,搜索并输出所有不同的素数和环
分析:
设a数组在前n个正整数中取值。为避免重复输出,约定第1个数[1]=1;
设置b数组标记奇素数,对指定正整数n,首先用试商判你别发,把2n范围内的奇素数标记为1
#include<stdio.h>
#include<math.h>
void main(){
int t,i,j,n,k,a[1000],b[500];
long s;
printf(" 请输入正整数n: ");
scanf("%d",&n);
if(n%2>0) //排除n为奇数时的求解
{printf( " 不存在%d项素数和环 \n",n);return; }
for(k=1;k<=2*n;k++) b[k]=0;
for(k=3;k<=2*n;k+=2)
{
for(t=0,j=3;j<=sqrt(k);j+=2)
if(k%j==0){ t=1;break;}
if(t==0) b[k]=1; //奇数k为素数的标记
}
a[1]=1;
s=0;
i=2;
a[i]=2;
while(1){
t=1;
for(j=1;j<i;j++)
if(a[j]==a[i] || b[a[i]+a[i-1]]!=1) //出现相同元素或非素数时返回
{ t=0;break; }
if(t&&i==n&&b[a[n]+1]==1) //确保首尾之和为素数
{
printf(" %ld: 1",++s);
for(j=2;j<=n;j++)
printf(",%d",a[j]);
printf("\n");
}
if(t&&i<n)
{
i++;
a[i]=2;
continue;
}
while(a[i]==n&&i>1) i--; //实施迭代回溯
if(i>1) a[i]++;
else break;
}
if(s==0)
printf(" 没有搜索到素数和环 \n");
else
printf(" 前%d个正整数组成以上%ld个素数和环 \n",n,s);
}
5.动态规划
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
剪绳子问题
给你一根长度为N的绳子,请把绳子剪成M段(m,n都是整数),每段绳子的
长度记为k[0],k[1],k[2]…. 请问如何剪绳子使得k[0],k[1],k[2]
的乘积最大
首先定义函数f(n)为把长度为n的绳子剪成若干段后各段长度乘积的最大值。在剪第一刀时,我们有n-1种选择,也就是说第一段绳子的可能长度分别为1,2,3.....,n-1。因此f(n)=max(f(i)*f(n-i)),其中0<i<n。
这是一个自上而下的递归公式。由于递归会有大量的不必要的重复计算。一个更好的办法是按照从下而上的顺序计算,也就是说我们先得到f(2),f(3),再得到f(4),f(5),直到得到f(n)
当绳子的长度为2的时候,只能剪成长度为1的两段,所以f(2) = 1,当n = 3时,容易得出f(3) = 2;
---------------------
public class Test{
public static void main(String[] args) {
System.out.println(maxAfterCutting(8));
}
/**
* 常规的需要O(n2)的时间复杂度和O(n)的空间复杂度的动态规划思路
* 题目的意思是:绳子至少是2米,并且必须最少剪一刀。
*/
public static int maxAfterCutting(int length){
if(length<2)
return 0;
if(length==2)
return 1;
if(length==3)
return 2;
// 子问题的最优解存储在f数组中,数组中的第i个元素表示把长度为i的绳子剪成若干段后各段长度乘积的最大值。
int[] f = new int[length+1];
f[0] = 0;
f[1] = 1;
f[2] = 2;
f[3] = 3;
int result = 0;
for(int i = 4;i<=length;i++){
int max = 0;
for(int j = 1;j<=i/2;j++){
int num = f[j]*f[i-j];
if(max<num)
max = num;
f[i] = max;
}
}
result = f[length];
return result;
}
}