经典算法设计与分析
递归
函数在运行时调用自身,并且一定要包含条件语句,在合适的时候终止递归
//2013年算一个M的N次方,要求用递归;
public class recursion {
public static void main(String[] args){
System.out.println(f(5,3));
}
public static Integer f(int M,int N){
if(N==1){
return M;
}
else {
return M*f(M,N-1);
}
}
}
// 用递归求f(n)=f(n-1)+f(n-2) n>2;n=1时 f(n)=1;n=2 时 f(n)=1;
public static void main(String[] args) {
System.out.println(f(5));
}
public static int f(int n){
//注意这里n==2与n==1在一个判断式内,因为n>2时f(n-1)+f(n-2)才成立
if(n==1||n==2){
return 1;
}
else {
return f(n-1)+f(n-2);
}
}
迭代法
有一定规律,每次循环都是从上次运算结果中获得数据,本次运算的结果都要为下次运算作准备
//求阶乘!(6的阶乘)
public static void main(String[] args) {
System.out.println(calculate(6));
}
public static int calculate(int n){
int result=1;
for (int i=n;i>=1;i--){
result=result*i;
}
return result;
}
分治法
分治法解决问题大多要利用递归函数,不断递归(
注意递归需要有终止条件
)才能得到问题的最小解,然后递归回溯得到原问题的解
归并算法
归并算法的关键点在于并即合并数组的部分,分的操作对array数组无实质上的影响。
该算法要注意的要点在于递归的顺序以及两子序列合并时的初始值
这里借助temp数组来完成子序列合并时的数据转移,并在合并完成后将temp数组赋值给array数组
图解:
public static void main(String[] args) {
int[] array= {
8,4,5,7,1,3,6,2};
int[] temp = new int[array.length];
sort(0,array.length-1,array,temp);
System.out.println(Arrays.toString(array));
}
//划分array
public static void sort(int left,int right,int[] array,int[] temp){
if (left<right){
int mid=(left+right)/2;
//划分左序列
sort(left,mid,array,temp);//左序列为从left到mid
//划分右序列
sort(mid+1,right,array,temp);
//合并上面划分的两端子序列
merge(left,mid,right,array,temp);
}
}
//合并左右两个子序列
public static void merge(int left,int mid,int right,int[] array,int[] temp){
int i=left;//初始化左边序列索引
int j=mid+1;//初始化右边序列索引
int t=0;//指向temp数组的索引
while (i<=mid&&j<=right){
if(array[i]<=array[j]){
temp[t]=array[i];
t++;
i++;
}else {
temp[t]=array[j];
j++;
t++;
}
}
while (i<=mid){
temp[t]=array[i];
i++;
t++;
}
while (j<=right){
temp[t]=array[j];
t++;
j++;
}
//将左右两个有序序列形成的temp赋值给array
t=0;
int templeft=left;
while (templeft<=right){
array[templeft]=temp[t];
templeft++;
t++;
}
}
快速排序
快速排序重点在于数据的替换顺序,将左序列中大于pivot的元素赋值给右序列中的r;将右序列中小于pivot的元素赋值给左序列中的l;并且最后要将基准元素归位
public static void main(String[] args) {
int[] array = {
5, 1, 7, 3, 1, 6, 9, 4};
partition(array,0,array.length-1);
System.out.println(Arrays.toString(array));
}
public static void partition(int[] array,int left,int right){
if (left >= right) {
return;
}
int l = left;
int r = right;
//以排序的第一个元素为基准
int pivot = array[left];
while (l<r){
//从右往左扫描,找到第一个比基准值小的元素
while (l<r&&array[r]>=pivot){
r--;
}
//将找到的元素与左边l指向的值替换
array[l]=array[r];
//从左往右扫描,找到第一个比基准值大的元素
while (l<r&&array[l]<=pivot){
l++;
}
//将找到的元素与右边r指向的值替换
array[r]=array[l];
}
//基准归位
array[l]=pivot;
//对基准值左边的元素进行递归排序
partition(array,left,l-1);
//对基准值右边的元素进行递归排序。
partition(array,r+1,right);
}
最大字段和问题
序列(-20,11,-4,13,-5,-2),最大字段和为20
采用分治法
在计算最大字段和时主要有三种情况:
- 最大字段和在左边
- 最大字段和在右边
- 最大字段和在包含center的中间位置
故分别计算左右两边字段和,再计算中间字段和,最后进行比较即可
public static void main(String[] args){
int[] arr = {
-20,11,-4,13,-5,-2};
System.out.println(maxSubSum(arr,0,arr.length-1));
}
public static int maxSubSum(int[] arr,int left,int right){
int sum=0;
//这是递归调用必须要有的终值情况。
if(left==right){
sum=(arr[left]>0?arr[left]:0);
}
else {
int center = (left+right)/2;
//求出左序列最大字段和
int leftSum = maxSubSum(arr,left,center);
//求出右序列最大字段和
int rightSum = maxSubSum(arr,center+1,right);
//求跨前后两端的情况,从中间分别向左右两端扩展
//从中间向左扩展,注意中间往左的第一个必然包含在内
//ls用来保存左边和,必须要遍历至left,因为算的是左边子序列的最大字段和
//lefts作为辅助变量来保存运算过程中得到的值,若大与ls则赋值给ls
int ls=0,lefts=0;
for(int i=center;i>=left;i--){
lefts+=arr[i];
if(lefts>ls){
ls=lefts;
}
}
//从中间向右扩展,center的后一个必然包含在内,必须要遍历至right,因为算的是右边子序列的最大字段和
//rights作为辅助变量来保存运算过程中得到的值,若大与rs则赋值给rs
int rs=0,rights=0;
for(int i=++center;i<=right;i++){
rights+=arr[i];
if(rights>rs){
rs=rights;
}
}
sum=ls+rs;
if(sum<leftSum){
sum=leftSum;
}
if(sum<rightSum){
sum=rightSum;
}
}
return sum;
}
棋盘覆盖问题
重点在于涂色的方法
采用分治法,将大棋盘划分为四个小棋盘,然后进行递归:
- 第一步,处理左上角棋盘
- 第二步,处理右上角棋盘
- 第三步,处理左下角棋盘
- 第四步,处理右下角棋盘
注意在处理过程中要判断特殊块是否在此子棋盘内,若在,在递归调用;若不在,则在对应位置填充特殊块,再调用
public class Qipan {
//初始化数组
int[][] board = new int[100][100];
//L形块编号
int tile = 1;
public void Chessboard(int tr,int tc,int dr,int dc,int size){
if(size==1){
return;
}
//将棋盘分为四个子棋盘
int s=size/2;
//t用来给定当前子块的L形块的编号
int t = tile++;
//第一步处理左上角棋盘
//左上角棋盘中包含特殊块
if(dr<tr+s&&dc<tc+s){
Chessboard(tr,tc,dr,dc,s);
}else {
//若左上角棋盘中不包含特殊块,则将该子棋盘的右下角覆盖为特殊块
board[tr+s-1][tc+s-1]=t;
Chessboard(tr,tc,tr+s-1,tc+s-1,s);
}
//第二步,处理右上角棋盘
//如果右上角棋盘右特殊块
if(dc>=tc+s&&dr<tr+s){
Chessboard(tr,tc+s,dr,dc,s);
}else {
//如果右上角棋盘中不包含特殊块,则将该子棋盘的左下角覆盖为特殊块
board[tr+s-1][tc+s]=t;
Chessboard(tr,tc+s,tr+s-1,tc+s,s);
}
//第三步,处理左下角棋盘
//如果左下角棋盘有特殊块
if(dr>=tr+s&&dc<tc+s){
Chessboard(tr+s,tc,dr,dc,s);
}else {
//如果左下角棋盘无特殊块,则将该子棋盘的右上角覆盖为特殊块
board[tr+s][tc+s-1]=t;
Chessboard(tr+s,tc,tr+s,tc+s-1,s);
}
//第四步,处理右下角棋盘
//如果右下角棋盘有特殊块
if(dr>=tr+s&&dc>=tc+s){
Chessboard(tr+s,tc+s,dr,dc,s);
}else {
//如果右下角棋盘无特殊块,则将该子棋盘的左上角覆盖为特殊块
board[tr+s][tc+s]=t;
Chessboard(tr+s,tc+s,tr+s,tc+s,s);
}
}
public static void main(String[] args) {
Qipan qipan = new Qipan();
//棋盘的大小
int size = 8;
//棋盘的初始坐标
int tr=0,tc=0;
//特殊块的位置
int dr=1,dc=3;
qipan.Chessboard(tr,tc,dr,dc,size);
for(int i = 0 ;i < size;i++){
for(int j = 0 ; j < size;j++){
System.out.print(String.format("%5d",qipan.board[i][j]));
}
System.out.println();
}
}
}
最近对问题
【问题】:
最近对问题要求在包含有n个点的集合S中,找出距离最近的两个点。设 p1(x1,y1),p2(x2,y2),……,pn(xn,yn)是平面的n个点。
严格地将,最近点对可能不止一对,此例输出一对即可。
想法:
采用分治法,这道题的思想类似于最大字段和问题。通过递归求左右两边的最小值d,再将x=mid的左右两边与mid距离相差d的点集加入到集合P,求是否有两点分布在mid左右两侧使其距离最小。最后再将中将2d部分求出的距离d3与左右两边求出的距离进行比较,从而求出最小距离
public static void main(String[] args) {
int n =4;
Point[] S = new Point[n];
for(int i=0;i<n;i++){
S[i]=new Point(i,i);
}
System.out.println("最近点距离为:"+Closet(S,0,n-1));
}
public static double Closet(Point[] S,int low,int high){
double d1,d2,d3,d;
if(high-low==1){
//当集合中只有两个点时
return Distance(S[low],S[high]);
}
//当集合中只有三个点时
if(high-low==2){
d1=Distance(S[low],S[low+1]);
d2=Distance(S[low],S[high]);
d3=Distance(S[low+1],S[high]);
d = (d1>d2)?d1:d2;
if(d>d3){
d=d3;
}
return d;
}
//当集合中拥有三个以上的点时
int mid = (low+high)/2;
//递归求左右两边
d1=Closet(S,low,mid);
d2=Closet(S,mid+1,high);
Point[] P = new Point[S.length];
for(int i=0;i<S.length;i++){
P[i]=new Point();
}
int pIndex=0;
d=Math.min(d1,d2);
//将在x=mid中心线两边且距离小于d的点给到集合P
for(int i=low;i<mid&&(S[i].x-S[mid].x<d);i++){
P[pIndex]=S[i];
pIndex++;
}
for(int i = mid+1;i<=high&&(S[i].x-S[mid].x<d);i++){
P[pIndex]=S[i];
pIndex++;
}
//以下本人认为无需再排序查找最小距离,而是可以直接用暴力法,求出集合P中的最小距离
//对集合P的y坐标从小到大排序
for(int i=0;i<pIndex;i++){
for (int j=i+1;j<pIndex;j++){
if(P[i].y>P[j].y){
Point temp = P[i];
P[i]=P[j];
P[j]= temp;
}
}
}
//遍历P中所有点,查找最小距离
for(int i=0;i<pIndex;i++){
for(int j=i+1;j<pIndex;j++){
//超出y坐标的范围d
if (P[i].y-P[j].y>d){
break;
}
else {
d3=Distance(S[i],S[j]);
d=Math.min(d3,d);
}
}
}
return d;
}
public static double Distance(Point p1,Point p2){
return Math.sqrt(Math.pow(p1.x-p2.x,2)+Math.pow(p1.y-p2.y,2));
}
public static class Point{
private int x;
private int y;
//省略构造方法
}
循环赛程(分治法)
https://blog.csdn.net/wly_2014/article/details/51388263
采用分治法将所有参加比赛的选手分成两部分,n=2^k 个选手的比赛日程表就可以通过n=2^(k-1)个选手的的比赛日程表来决定。递归的执行这样的分割,直到只剩下两个选手,比赛日程表的就可以通过这样的分治策略逐步构建。
该算法的核心在于发现如下规律:
初始的2*2数组排列为【1,2】,【2,1】
每个数组分为四份,左上角的值=右下角的值,左下角的值=右上角的值,左下角的值为左上角对应的值+Math.pow(2,i-1),
i为当前循环次数,从2(2代表构成四人的赛程)开始,从k(k代表构成n=2^k人的赛程)结束
减治法
减治法:把一个大问题划分为若干个子问题,但是这些子问题不需要分别求解,只需求解其中的一个子问题,因而也无需对子问题的解进行合并
计算两个序列的中位数
问题描述:
现有两个等长的升序序列的序列A,B,试设计一个时间和空间都尽可能高效的算法,找出两个序列的中位数
算法的基本思想是:分别求出两个序列的中位数,即为a b,有下列三种情况
1:a=b;即a 为两个序列的中位数
2:a<b: 则中位数只能出现在a和b之间,在序列A中舍弃a之前的元素的到序列A1,在序列B中舍弃b之后的元素,得到序列B1
3:a>b:则中位数只能出现在b和a之间,在序列a中舍弃A之后的元素得到序列A1,在序列B中舍弃b之前的元素,得到B1;
在A1和B1中分别求出中位数,重复上述过程,得到俩个序列中只有一个元素,则较小者即为所求
注意:在利用减治法求两个序列中位数的过程中,划分后两个子序列的长度相同
若A的中位数小于B的中位数,则要使
if((s1+e1)%2==0) s1=mid1;
else s1=mid1+1;若B的中位数小于A的中位数,则要使
if((s2+e2)%2==0) s2=mid2;
else s2=mid2+1;
public static void main(String[] args) {
int[] a = {
11,13,15,17,19};
int[] b = {
2,4,10,15,20};
System.out.println(middleSerach(a,b));
}
public static int middleSerach(int[] a,int[] b){
int n = a.length;
//初始化两个序列的上下界
int s1,e1,s2,e2;
s1=0;
e1=n-1;
s2=0;
e2=n-1;
int mid1,mid2;
while (s1<e1&&s2<e2){
//分别求本次循环的子序列的两个中位数
mid1 = (s1+e1)/2;
mid2 = (s2+e2)/2;
//如果此时两个序列的中位数相同,则直接返回
if(a[mid1]==b[mid2]){
return a[mid1];
}
//如果左边序列的中位数小于右边序列的中位数
if(a[mid1]<b[mid2]){
//确保两个序列的大小相等
if((s1+e1)%2==0) s1=mid1;
else s1=mid1+1;
e2=mid2;
}
//如果右边序列的中位数小于左边序列的中位数
else {
//将右边的子序列调整为与左边子序列相等的大小
if((s2+e2)%2==0) s2=mid2;
else s2=mid2+1;
e1=mid1;
}
}
//返回a,b数组中值最小的,即为中位数
if(a[s1]<b[s2]) return a[s1];
return b[s2];
}
折半查找
折半查找也称为二分查找,是一种效率较高的查找方法,查找时要求表中的节点按关键字的大小排序,并且要求线性表顺序存储。
- 首先用要查找的关键字值(key)与中间位置结点的关键字值(arr[mid])相比较;若比较结果相等,则查找完成
- 若待查关键值大于中间结点的关键字值(key > arr[mid])),则应查找右序列,否则(key < arr[mid])),查找左序列
- 重复步骤1~3,直到找到满足条件的结点,或者明确表中没有这样的结点。
public static void main(String[] args) {
int[] array = {
7,14,18,21,23,29,31,35,38};
//搜索18的位置
int position = binarySearch(array,18);
if(position==-1)
System.out.println("array数组中不存在此元素");
else
System.out.println("18在array数组中的位置为:"+position);
}
public static int binarySearch(int[] array,int target){
//初始化low,high的位置
int low = 0,high = array.length-1;
//定义mid
int mid;
//减治法核心,逐步较小搜索范围
while (low<=high){
//定位mid
mid = (low+high)/2;
//如果array[mid]==target,则返回mid
if(array[mid]==target) return mid;
//如果array[mid]>target,则搜索左边序列
else if(array[mid]>target) high=mid-1;
//搜索右边序列
else low=mid+1;
}
//查找失败
return -1;
}
二叉查找树
二叉搜索树,是指一棵空树或者具有下列性质的二叉树:
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
- 任意节点的左,右子树也分别为二叉搜索树;
- 没有键值相等的节点。
【问题】在一个无序序列中执行查找操作,可以将无序序列建立一棵二叉查找树,然后再二叉查找树中查找值为k的记录,若查找成功,返回记录k的存储位置,若查找失败,返回空指针
【思想】
- 若root使空树,则查找失败
- 若k=根节点的值,则查找成功
- 若k<根节点的值,则在root的左子树中查找
- 若k>根节点的值,则再root的右子树中查找
public static void main(String[] args) //unit test
{
BinarySearchTree tree=new BinarySearchTree();
tree.insert(39);
tree.insert(24);
tree.insert(64);
tree.insert(23);
tree.insert(30);
tree.insert(53);
tree.insert(60);
BinarySearchTree.Node node = tree.find(64);
System.out.println(node.data);
}
public static class BinarySearchTree
{
// 建立二叉树
private class Node
{
// 节点类
int data; // 数据域
Node right; // 右子树
Node left; // 左子树
}
private Node root; // 树根节点
//插入节点,创建二叉排序树
public void insert(int key)
{
Node p=new Node(); //待插入的节点
p.data=key;
if(root==null)
{
root=p;
}
else
{
Node parent=new Node();
Node current=root;
while(true)
{
//parent暂存current
parent=current;
if(key>current.data)
{
current=current.right; // 右子树
//若右子树为空,则直接将p给到右子树
if(current==null)
{
parent.right=p;
return;
}
}
else //本程序没有做key出现相等情况的处理,暂且假设用户插入的节点值都不同
{
current=current.left; // 左子树
//若左子树为空,则直接将p给到左子树
if(current==null)
{
parent.left=p;
return;
}
}
}
}
}
//查找指定值
public Node find(int target){
Node current = root;
while (current.data!=target){
if(current.data<target){
current=current.right;
}
else {
current = current.left;
}
if (current==null){
return null;
}
}
return current;
}
}
判断一棵二叉树是否为平衡二叉树
public bool IsBalanced_Solution(TreeNode pRoot)
{
if(pRoot==null)//如果当前树为空,理所当然是平衡二叉树
{
return true;
}
int leftHigh=getDepth(pRoot.left);//不为空,求出左右子树的高度,求高度差
int rightHigh=getDepth(pRoot.right);
int gap=leftHigh-rightHigh;
if(gap>1 || gap<-1)//若高度差的绝对值大于1,就不是平衡二叉树,返回false
{
return false;
}
return IsBalanced_Solution(pRoot.left)&&IsBalanced_Solution(pRoot.right);//否则继续递归地求解,看其左右子树是否为平衡二叉树
}
public int getDepth(TreeNode root) {
if( root == null ) return 0;
int left = getDepth(root.left);
int right = getDepth(root.right);
//
return ( left > right ? left : right ) + 1;
}
排序问题中的减治法
插入排序
【题目】:
应用插入排序方法对一个记录序列进行升序排列
想法:
在i-1个记录的有序区r[1]~r[i-1]中插入一个记录r[i]。采用顺序查找的方法查找待插入的位置,从i=2开始循环。注意每次循环都要设置a[0]=a[j]哨兵防止越界,再自i-1起向后查找,并向后移元素。
//实现插入排序
public void insertSort(int[] array,int n){
//从第二个位置开始执行插入排序
for(int i=2;i<=n;i++){
//设置哨兵,防止下面的一层循环越界
array[0]=array[i];
int j;
for(j=i-1;array[j]>array[i];j--){
array[j+1]=array[j];
}
//因为上面的循环再结束的最后一步又j--了一次
array[j+1]=array[0];
}
}
堆排序
【题目】:
应用堆排序方法对一个记录序列进行升序排列。
想法:
重点在于建堆的方法,注意当前根节点的子堆初始化完成后还要遍历发生替换的左子堆或右子堆,这样才能完成对节点k的建堆
//初始化堆
public void Sift(int[] array,int k,int n){
//i指向对根节点为i的子堆进行初始化
int i=k;
//根节点为i的子堆的左孩子
int j=2*i+1;
while(j<n){
//如果右孩子的值大于左孩子的值,则j指向较大的节点
if(array[j+1]>array[j]) j++;
if(array[j]<array[i]) break;//根节点为i的子堆初始化完成
else{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
//再去遍历发生替换的子堆,调整成大根堆
i=j;
j=2*i+1;
}
}
}
public void heap(int[] array,int n){
//最后一个分支节点下标为(n-1)/2
int k = (n-1)/2;
//开始建堆
for(int i=k;i>=0;i--){
Sift(array,i,n);
}
//将建好的堆逐个将根节点出堆,然后最后面的节点移如根节点
for(int i=1;i<n;i++){
System.out.println(array[i]+" ");
swap(array[0],array[n-i]);//交换堆顶元素与堆底元素
Sift(array,0,n-i)
}
}
组合问题中的减治法
淘汰赛冠军
【题目】:
假设n=2^k个选手进行竞技淘汰赛,最后决出冠军的选手,请设计淘汰赛的规则
想法:
采用减治法,将所有n个选手分为n/2组选手,两两进行比赛,然后再将剩余选手分成n/4组,再两两进行比赛。。。以此类推
public int Game(int[] r,int n){
int i=n;
while(i>1){
//分组
i=i/2;
for(int j=0;j<i;j++){
//如果j+i战胜j,则将j+i给到j
if(Comparable(r[j],r[j+i])){
r[j]=r[j+i];
}
}
}
return r[0];
}
假币问题
【题目】:
在n枚外观相同的硬币中,有一枚是假币,并且已知假币较轻,挑出假币。
想法:
将n个硬币分成3组,其中前两组有n/2+1枚,后一组有n-num1-num2枚。然后将前两组放到天平上,如果重量相同,则假币一定在第三组中,否则在这两组中
int[] a={
2,2,1,2,2,2,2,2};
public int Coin(int n,int low,int high){
int i,num1,num2,num3;
int add1=0,add2=0;
if(n==1) return low+1;//如果只有一枚硬币,返回的是序号即下标+1
if(n%3==0) num1=num2=n/3;//正好可以分成三份
else{
num1=num2=n/3+1;
}
num3=n-num1-num2;
for(int i=0;i<num1;i++)
add1+=a[i];
for(int j = num1;j<num1+num2;j++){
add2+=a[j];
}
if(add1==add2){
return Coin(num3,low+num1+num2,high);
}
else if(add1>add2)
return Coin (num2,num1+low,low+num1+num2-1);//注意下标范围要减1
else
return Coin(num1,low,low+num1-1);
}
贪心法
埃及分数
埃及分数是指分子是1的分数,也叫单位分数。古代埃及人在进行分数运算时。只使用分子是1的分数。因此这种分数也叫做埃及分数,或者叫单分子分数。如:6/7 = 1/2 + 1/3 + 1/42
埃及分数的核心点在于如何利用贪心算法每次都找到最大的分数使所有找到的分数的和等于原数的和
- 输入真分数的分子A和分母B
- E=B/A+1主要在于发现这个规律
- A=A*E-B,B=B*E
- 求A和B的最大公约数,如果R不为一,则将A和B同时除以R
- 如果A等于1,则输出1/B,算法结束;否则转b步骤2重复执行
public static void main(String[] args) {
EgyptFration(7,8);
}
public static void EgyptFration(int A,int B){
int E,R;
System.out.print(A+"/"+B+"=");
do{
E=B/A+1;
System.out.print("1/"+E+"+");
A=A*E-B;
B=E*B;
//求A,B的最大公约数
R=Gcd(A,B);
if(R>1){
A=A/R;
B=B/R;
}
}while(A>1);
System.out.print("1/"+B);
return;
}
/**
* 辗转相除法求最大公约数
* @param a 被除数
* @param b 除数
* @return 最大公约数
*/
public static int getGCD(int a, int b)
{
if (a % b == 0)
{
return b;
}else
{
return getGCD(b, a % b);
}
}
图问题中的贪心算法
TSP问题
旅行推销员问题(Travelling salesman problem, TSP)是这样一个问题:给定一组城市和每对城市之间的距离,问题是找到最短的可能路线,访问每个城市一次,然后返回起点。
哈密顿回路
哈密顿图(哈密尔顿图)(Hamiltonian graph,或Traceable graph)是一个无向图,由天文学家哈密顿提出,由指定的起点前往指定的终点,途中经过所有其他节点且只经过一次。在图论中是指含有哈密顿回路的图,闭合的哈密顿路径称作哈密顿回路(Hamiltonian cycle),含有图中所有顶点的路径称作哈密顿路径(Hamiltonian path)。
贪心策略:从任意城市出发,每次在没有到过的城市中选择最近的一个,直到经过了所有的城市,最后回到出发城市
public static void main(String[] args) {
int[][] arc = {
{
0, 10, 15, 20 },
{
10, 0, 35, 25 },
{
15, 35, 0, 30 },
{
20, 25, 30, 0 }
};
int tsp = tsp(arc, 0);
System.out.println();
System.out.println("TSPLength="+tsp);
}
//从顶点w开始
public static int tsp(int[][] arc,int w){
//初始化走过的边的条数
//初始化TSP长度
int edgeCount=0,TSPLength=0;
int min,u,v;
//flag标记顶点是否加入回路,0表示未加入回路
int[] flag = new int[arc.length];
for(int i =0;i<flag.length;i++){
flag[i]=0;
}
u=w;
v=0;
//将顶点u加入回路
flag[u]=1;
//若有arc.length-1则已构成回路
//注意这里循环次数<arc.length-1
while (edgeCount<arc.length-1){
min = 10000;
for (int j=0;j<arc.length;j++){
if(flag[j]==0&&arc[u][j]<min&&arc[u][j]!=0){
min = arc[u][j];
v=j;
}
}
TSPLength = TSPLength+arc[u][v];
flag[v]=1;
edgeCount++;
System.out.print(u+"->"+v+" ");
//重新将起点设为aru[u][v]的终点
u = v;
}
System.out.print(u+"->"+w);
return TSPLength+arc[u][w];
}
同时本题还有解法二
图着色问题
【问题】:给定无向连通图G=(V,E),求G的着色数k,使得用k种颜色对G中的顶点着色,可使任意两个相邻顶点着不同颜色
思想
(1)任选一顶点着颜色1,在图中寻找尽可能多的顶点用颜色1着色;
(2)任选未被颜色1着色的顶点,用颜色2着色,在图中寻找尽可能多的顶点用颜色2着色;
(3)依次选取颜色3,4…重复以上操作,直到所有顶点都被着色,即可停止算法。
public static void main(String[] args) {
int n=5;
int m=5;
int[][] arc={
{
-1,-1,-1,-1,-1,-1},{
-1,0,1,1,1,0},{
-1,1,0,1,1,1},{
-1,1,1,0,1,0},{
-1,1,1,1,0,1},{
-1,0,1,0,1,0}};
int i = coloGraph(arc);
System.out.println("共使用"+i+"种颜色着色");
}
public static int coloGraph(int[][] arc){
//色号初始化
int k = 0;
//flag = 1表示图中还有顶点未着色
int flag = 1;
//顶点i的色号用color[i]=?表示
int[] color = new int[arc.length];
for(int i : color){
//所有顶点均未涂色
color[i] = 0;
}
while (flag==1){
//更换下一种颜色
k++;
flag = 0;
///循环所有顶点,判断是否可以着色,贪心算法核心
for(int i = 0;i< arc.length;i++){
if(color[i]==0){
//先涂色,在判断涂色是否冲突
color[i] = k;
//如果涂色不冲突
if(Ok(i,arc,color)){
System.out.println("顶点"+i+"的涂色为"+k+" ");
}else {
//消去上面的涂色
color[i]=0;
//表示还有顶点未涂色
flag=1;
}
}
}
}
return k;
}
//判断涂色是否发生冲突
public static boolean Ok(int i,int[][] arc,int[] color){
for(int j = 0 ;j < arc.length;j++){
if(arc[i][j]==1&&color[i]==color[j]){
//涂色发生冲突
return false;
}
}
return true;
}
最小生成树问题
Prim算法
最小生成树即各边的权值之和最小的生成树。
采用贪心算法:
定义一个Element类,其中存放lowcost(最小权值)、adjvex(最小权值所在的临接节点)
定义一个数组Element[] shortEdge,记录节点i到节点adjvex的最小权值
- 从节点w开始初始化辅助数组shortEdge
- 将顶点w输出
- 开始n-1次循环,找到权值最小的n-1条边
- 在lowcost中选取最短边,取adjvex中对应的顶点序号k
- 输出顶点k和权值
- 根据顶点k调整shortEdge
public class Element{
int lowcost;
int adjvex;
}
//从顶点w开始生成最下生成树
public void Prim(int[][] arc,int n,int w){
Element[] shortEdge = new Element[n];
for(int i=0;i<n;i++){
shortEdge[i]=new Element();
}
int i,j,k,min;
//初始化shortEdge数组
for(i=0;i<n;i++){
shortEdge[i].lowcost = arc[i][w];
shortEdge[i].adjvex = w;
}
//将顶点w加入生成树中
shortEdge[w].lowcost=0;
for(i=0;i<n-1;i++){
min=1000;
for(j=0;j<n;j++){
if(shortEdge[j].lowcost<min&&shortEdge[j].lowcost!=0){
min = shortEdge[j].lowcost;
//用k来记录当前加入最小生成树的节点
k=j;
}
}
//打印当前加入最小生成树的节点
System.out.print(shortEdge[k].adjvex+" ")
shortEdge[k].lowcost=0;
//以节点k调整辅助数组
for(j=0;j<n;j++){
//是对的,可以画图知构成的最小生成树的最小权值取决于以下条件,因为前面的w到k已经是从w出来路径最小的节点了
if(shortEdge[j].lowcost>arc[k][j]&&shortEdge[j].lowcost!=0){
shortEdge[j].lowcost=arc[k][j];
shortEdge[j].adjvex = k;
}
}
}
}
组合问题中的贪心法
背包问题
【问题】 给定n个物品和一个容量为maxWeight的背包,物品i的重量为weight[i],其价值为val[i],背包问题是如何选择装入背包的物品,使得装入背包中物品的总价值最大。注意和0/1背包的区别,在背包问题中,可以将某种物品的一部分装入背包中,但不可以重复装入。
想法:
收益优先,收益越大的越提前放。
重量优先,重量越大的越提前放。
单位重量收益最大的物品先放入背包。即性价比最大的物品先放入
算法:
- 初始化weight,val数组,weightVal数组
- 将weightVal数组按照冒泡排序进行降序排列,并且在交换值得过程中要将index数组的值也交换,以此实现将性价比与商品重量和价值的绑定
- 按照weightVal的顺序依次将物品放入背包中,注意普通背包问题不可重复放入同意物品
- maxWeight>=weight[index[i]],则可直接将物品放入
- maxWeight>0&&maxWeight<weight[index[i]],则要放入物品的一部分
public static void main(String[] args) {
double [] weight={
20,30,10};
double[] val={
60,120,50};
greedyPackage(weight,val,50);
}
public static void greedyPackage(double[] weight,double[] val,int maxWeight){
int n = weight.length;
//按性价比存放i 的数组,index下标用来绑定性价比和该商品的重量weight[index[i]],表示在weightVal数组中i指针对应的index[i]为weight数组中该商品对应的重量(价值)
int[] index = new int[n];
//初始化性价比数组
double[] weightVal = new double[n];
for(int i = 0; i < n ;i++){
weightVal[i] = val[i]/weight[i];
//初始顺序
index[i] =i;
}
//按性价比进行排序,按降序排序
for(int i = 0 ;i < n;i ++){
for(int j = i +1;j<n;j++){
if(weightVal[i]<weightVal[j]){
double temp = weightVal[i];
weightVal[i] = weightVal[j];
weightVal[j] = temp;
//交换下标
int tempIndex = index[i];
index[i] = index[j];
index[j] = tempIndex;
}
}
}
//问题的解存放在数组x中
double[] x = new double[n];
int maxVal = 0;
for(int i = 0 ; i < n ; i ++){
if(maxWeight>=weight[index[i]]){
//将第i件商品放入背包中
x[i] = 1;
System.out.println(weight[index[i]]+"放入背包中"+",比例为"+x[i]);
maxWeight-=weight[index[i]];
maxVal +=val[index[i]];
}else if(maxWeight>0&&maxWeight<weight[index[i]]){
x[i] = maxWeight/weight[index[i]];
System.out.println(weight[index[i]]+"放入背包中"+",比例为"+x[i]);
maxWeight -= x[i]*weight[index[i]];
maxVal += x[i]*val[index[i]];
}else {
break;
}
}
//打印背包信息和最大价值
System.out.println("总共放下的物品有:"+ Arrays.toString(x));
System.out.println("背包里面物品的总价值为:"+maxVal);
}
活动安排问题
【问题】
在所给的活动集合中选出一个最大的相容活动子集和。
设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源(如一个阶梯教室等),而在
同一时间内只有一个活动能使用这一资源。
- 每个活动i都有一个要求使用该资源的起始时间startTime和一个结束时间endTime,
且0<=startTime < endTime。 - 如果选择了活动i,则它在半开时间区间[startTime, endTime)内占用资源。
- 若区间[[startTimei, endTimei)与区间[[startTimej, endTimej)不相交,则称活动i与活动j是相容的。也就是说,当startTimei ≥ endTimej 或 endTimei ≥ startTimej时,活动i与活动j相容。
想法:贪心选择当前活动集合中结束时间最早的活动就归结为取当前活动集合中排在最前面的活动
- 定义s(记录i项活动的开始时间),f(记录i项活动的结束时间),a(用来表示活动的状态)
- 将s按照升序排序,同时响应的调整f和a
- 首先访问s中的第一个活动,再循环n-1次访问剩余活动
- 如果f[j]<s[i],表示活动i与活动j相容,访问活动i
- 不访问活动i
public static void main(String[] args) {
// 定义11项活动的开始时间,结束时间(按升序排序)
int[] s = {
0,1,3,0,5,3,5,6, 8, 8, 2, 12};// s[0]不用
int[] f = {
0,4,5,6,7,8,9,10,11,12,13,14};// f[0]不用
boolean[] a = new boolean[12];// a[0]不用
greedySelector(s,f,a);
}
public static void greedySelector(int[] s,int[] f,boolean[] a){
a[1] = true;//选择安排活动1
//总的安排活动数
int count = 1;
//记录最近一次的活动序号
int j= 1;
int n = s.length;
for(int i = 2;i<n;i++){
//如果第i项活动相容
if(f[j]<s[i]){
a[i] = true;//安排第i项活动
j = i;//记录最近一次的活动序号j
count++;//活动次数++
}else {
//第i项活动不相容
a[i] =false;
}
}
for (int i=1; i<a.length; i++)
System.out.println("A" + i + " = " + a[i]);
System.out.println("总的活动数量="+count);
}
多机调度问题
【题目】:
假设有n个任务由m个可并行工作的机器完成。完成任务i需要的时间为t[i]。试设计一个算法找出完成这n个任务的最佳调度,使得完成全部任务的时间最早。
思想:贪心策略:把处理时间最长的作业优先分配给最先空闲的机器
- 将s数组按照降序排列,初始化S[m][n],d[m]表示机器m的空闲时间,rear[m]表示机器m队列队尾序号
- 分配m个作业个m个机器
- 分配n-m个作业给最先空闲的机器
public static void main(String[] args) {
int[] t= {
16,14,6,5,4,3,2};
int[] d = new int[t.length];
for(int i = 0 ;i < t.length;i++){
d[i]=0;
}
MultiMachine(t,7,d,3);
}
public static void MultiMachine(int[] t,int n, int[] d,int m){
//假设三台机器处理7个作业
int[][] S = new int[3][7];
for(int l=0;l<3;l++){
for (int q = 0;q<7;q++){
S[l][q]=-1;
}
}
//表述机器i的队列中最后一个元素的下标
int[] rear = new int[3];
int i,j,k;
//将m个作业给到m个机器
for(i = 0 ; i < m;i++){
d[i]=t[i];
rear[i]=0;
S[i][0]=i;
}
//安排其于n-m个作业
for(i=m;i<n;i++){
//找到最先空闲的机器
for(j=0,k=1;k<m;k++){
if(d[k]<d[j]) j=k;
}
//将j机器的队尾序列加一
rear[j]++;
//将i作业给到S
S[j][rear[j]]=i;
d[j]=d[j]+t[i];
}
for(i = 0;i<m;i++){
System.out.print("机器"+i+":");
for(j=0;S[i][j]>=0;j++){
System.out.print("作业"+S[i][j]+" ");
}
System.out.println();
}
}
https://blog.csdn.net/weixin_43404388/article/details/106738165
回溯法
素数环问题
【问题】:
输入正整数n,把整数1,2,3……,n组成一个环,使得相邻两个整数之和均为素数。
想法:
对每个位置从1开始试探,约束条件是正在试探的数满足如下条件:
- 与已经填写到数组中的数不能重复
- 与前面相邻的整数之和是一个素数
- 最后一个填写到素数环中的整数与第一个填写的整数之和是一个素数
填写第k个位置时,如果满足上述条件,则继续填写k+1个位置。如果1-n都无法填写到第k个位置,则取消对第k个位置的填写,回溯到第k-1个位置
public static void main(String[] args) {
int i ,k;
int n = 6;//有5个数:1,2,3,4,5
int[] a = new int[n];
//初始化a数组
for(i=0;i<n;i++){
a[i]=0;
}
//指定第0个位置为1
a[0]=1;
k=1;
while (k>=1){
a[k] = a[k]+1;
while (a[k]<=n){
//若位置k可以填写整数a[k]
if(Check(k,a,n)==1){
break;
}else
a[k]=a[k]+1;//试探下一个数
}
if(a[k]<=n&&k==n-1){
for(i=0;i<n;i++){
System.out.print(a[i]+" ");
}
return;
}
if(a[k]<=n&&k<n-1){
k = k+1;//填写下一个位置
}
else
a[k--]=0;//回溯到k-1的位置,并置a[k]=0
}
}
//判断位置k的填写是否满足约束条件
static int Check(int k,int[] a,int n){
int flag=0;
for(int i = 0; i < k;i++){
//若已经填入重复数,则不可填写
if(a[i]==a[k]) return 0;
}
flag = Prime(a,a[k]+a[k-1]);
//判断最后一个数和第一个数之和是否为素数
if(flag==1&&k==n-1){
flag = Prime(a,a[k]+a[0]);
}
return flag;
}
//判断相邻两个数之和是否为素数
static int Prime(int[] a,int x){
//求k的平方根
int n =(int)Math.sqrt(x);
for(int i =2;i<=n;i++){
//若不为素数
if(x%i==0) return 0;
}
//为素数
return 1;
}
图问题中的回溯法
图着色问题
【问题】:
给定无向连通图G=(V,E),求G得做小色数k,使得用k种颜色对G中的顶点着色,可使任意两个相邻顶点着不同颜色
思想:
从顶点0开始用颜色1开始试探,约束条件是正在试探的数满足如下条件:
- 顶点的涂色满足Ok函数,即给顶点k涂色color[k]使其与其他顶点不冲突
- 若是冲突,则更换下一种颜色
- 若不冲突,则试探下一个节点,此时也从颜色1开始
注意:算法中的回溯是指当节点k所涂颜色超过m时会进行,会取消对k节点的涂色,回溯到k-1节点,再进行试探
public static void main(String[] args) {
int n=5;
int m=5;
int[][] arc={
{
-1,-1,-1,-1,-1,-1},{
-1,0,1,1,1,0},{
-1,1,0,1,1,1},{
-1,1,1,0,1,0},{
-1,1,1,1,0,1},{
-1,0,1,0,1,0}};
GraphColor(arc,6);
}
/**
*
* @param arc 图
* @param m m种颜色
*/
public static void GraphColor(int[][] arc,int m){
int i ,k;
int n =arc.length;
int[] color = new int[n];
//初始化每个顶点的颜色
for(i=0;i<n;i++){
color[i]=0;
}
k=0;
while (k>=0){
color[k] = color[k]+1;//选择下一种颜色
while (k<=n){
if(Ok(k,color,arc)==1) break;
else color[k] = color[k]+1;//选择下一种颜色
}
//如果所有节点都涂色
if(color[k]<=m&&k==n-1){
for(i=0;i<n;i++){
System.out.println("顶点"+i+"的颜色为:"+color[i]);
}
return;
}
//处理下一个顶点
if(color[k]<=m&&k<n-1){
k++;
}
else {
color[k--]=0;//回溯
}
}
}
//判断k的涂色是否发生冲突
static int Ok(int k,int[] color,int[][] arc){
for(int i = 0;i<k;i++){
if(arc[k][i]==1&&color[i]==color[k]){
return 0;
}
}
return 1;
}
哈密顿回路问题
【问题】:
哈密顿图(哈密尔顿图)(英语:Hamiltonian path,或Traceable path)是一个无向图,由天文学家哈密顿提出,由指定的起点出发回到该起点,途中经过所有其他节点且只经过一次。在图论中是指含有哈密顿回路的图,闭合的哈密顿路径称作哈密顿回路(Hamiltonian cycle),含有图中所有顶点的路径称作哈密顿路径。
想法:
定义x数组记录回路,visited数组记录节点的访问位,采用回溯法的思想,从节点0开始试探如下条件
- 如果顶点x[k]未被访问且顶点<x[k-1],x[k]>有边,则将该点加入回路,并且搜索下一个节点(即其子节点)
- 否则在剩余的节点中寻找符合1的节点,如果没有,则进行回溯,即回到节点k的父节点寻找其父节点的兄弟节点
注意:
- 要定义回溯结束的条件x[k]<n&&k==n-1&&arc[x[k]][0]==1,即最后一个节点到第一个节点有边且k遍历到最后一个节点
- 回溯操作
public static void main(String[] args) {
int[][] arc={
{
-1,-1,-1,-1,-1,-1},{
-1,0,1,1,1,0},{
-1,1,0,1,1,1},{
-1,1,1,0,1,0},{
-1,1,1,1,0,1},{
-1,0,1,0,1,0}};
int[][] arc2={
{
0,1,0},{
1,0,1},{
0,1,0}};
Hamiton(arc2);
}
public static void Hamiton(int[][] arc){
int n = arc.length;//顶点个数
int[] x = new int[n];//x表示哈密顿回路经过的点
int i ,k;
int[] visited= new int[10];//假设图最多有10个顶点
//初始化回路经过的顶点数组和顶点的标志数组
for( i = 0 ; i < n ;i ++){
x[i]=0;
visited[i]=0;
}
//从顶点0出发
x[0]=0;
visited[0]=1;
k = 1;
while (k>=1){
x[k] = x[k]+1;//搜索下一个顶点
while (x[k]<n){
if(visited[x[k]]==0&&arc[x[k-1]][x[k]]==1) break;//如果顶点x[k]未被访问且顶点<x[k-1],x[k]>有边,则将该点加入回路
else x[k] = x[k]+1; //搜索下一个顶点
}
//如果搜索完所有节点且最后一个节点与第一个节点之间存在回路,则输出可能解
if(x[k]<n&&k==n-1&&arc[x[k]][0]==1){
for (k = 0 ;k < n ; k ++){
System.out.print(x[k]+" ");
}
break;
}
if(x[k]<n&&k<n-1){
visited[x[k]]=1;//标记顶点x[k]被访问
k++;//开始寻找k+1个访问节点
}
else {
//回溯
visited[x[k]]=0;
x[k]=0;
k=k-1;
}
}
}
组合问题中的回溯法
N皇后问题
【题目】:
N皇后问题 。在N×N的棋盘上放置N个皇后,且要求任意两个皇后不能在同行同列同对角线上。输入N,输出能摆放成功的情况数。
想法:
使用回溯法,定义x[k]数组用来表示皇后的位置在第k行第x[k]列,count表示可能解的数目
- 对k行元素每次从第0列开始进行试探,如果发生冲突,则试探下一列
- 如果未发生冲突,则将皇后放置在x[k]位置,然后判断下面的三个条件
- 如果正好填充至N-1个皇后,则输出可能解注意,题目要求可能解的数目,因此输出完成后不可返回,要count++
- 如果未填充至N-1个皇后,则继续填充第k++个皇后
- 如果填充列数大于N-1,则回溯至x[k–]=-1
提示:
本题要求可能解的数目,故对于循环while (k>=0),跳出的情况为k=-1,即第一行的列数为N的情况,此时所有的可能解的情况已经全部考虑到
public static void main(String[] args) {
int queen = Queen(4);
System.out.println(queen);
}
public static int Queen(int N){
//表示可能解的数目
int count = 0;
//x[i]数组用来表示皇后的位置在第i行第x[i]列
int[] x = new int[N];
int i,k;
//初始化位置数组为-1,因为每次都是从第0列开始试探
for(i=0;i<N;i++){
x[i]=-1;
}
k=0;
while (k>=0){
//试探下一列
x[k]++;
//发生冲突
while (x[k]<N&&Place(x,k)==0){
x[k]++;//k行皇后试探x[k]+1列
}
//输出可能解
if(x[k]<N&&k==N-1){
for (i=0;i<N;i++){
System.out.println("第"+(i+1)+"行皇后应该放在第"+(x[i]+1)+"列");
}
count++;
// return;
}
//试探下一行
if(x[k]<N&&k<N-1){
k++;
}
else {
//回溯
x[k--]=-1;
}
}
return count;
}
//判断皇后所摆的位置是否与之前的皇后位置发生冲突
public static int Place(int[] x,int k){
for(int i=0;i<k;i++){
if(x[i]==x[k]||Math.abs(i-k)==Math.abs(x[i]-x[k])){
return 0;
}
}
return 1;
}
批处理作业调度问题
【问题】:
有n个作业,每一个作业都有两项任务分别在2台机器上完成。每个作业必须先有机器1处理,然后再由机器2处理。作业i需要机器1的处理时间为a[i],需要机器2的处理时间为b[i]。对于一个确定的作业调度,设sum1[k]是第k个作业在机器1上完成处理时间,sum2[k]是第k个作业在机器2上完成处理时间。要求确定这n个作业的最优处理顺序,即完成这n个作业所用时间最少
想法:
用数组x[k]表示第k个处理的作业序号为x[k],sum1[k]数组表示机器1处理完第k个作业后的时间为sum1[k],sum2[k]数组表示机器2处理完第k个作业后的时间为sum2[k]。
- 初始化x,sum1,sum2数组
- 从第k个作业开始试探(初始k=1),如果判断前面的已确定序列中无x[k],则将该x[k]作业加入,计算sum1[k] = sum1[k-1]+a[x[k]]; sum2[k] = Math.max(sum1[k],sum2[k-1])+b[x[k]];
- 如果未调度至作业n,则调度第k++个作业
- 否则判断是否正好调度至作业n,如果是,则赋值BestTime,然后进行回溯
(注意这里的回溯与之前不同,在求出一个可能解后进行回溯,判断其他解情况的最小时间是否小于前面可能解的BestTime)
public static void main(String[] args) {
int[] a ={
2,5,4};
int[] b = {
3,2,1};
BatchJob(a,b,3);
}
public static int BatchJob(int[] a,int[] b,int n){
int BestTime = Integer.MAX_VALUE;
int i,k;
//用数组x[k]表示第k个处理的作业序号为x[k]
int[] x = new int[10];
//sum1[k]数组表示机器1处理完第k个作业后的时间为sum1[k]
int[] sum1 = new int[10];
//sum2[k]数组表示机器2处理完第k个作业后的时间为sum2[k]
int[] sum2 = new int[10];
for(i=1;i<=n;i++){
x[i]=-1;
sum1[i]=0;
sum2[i]=0;
}
k = 1;//调度第1个作业
sum1[0]=0;
sum2[0]=0;
while (k>=1){
x[k]++;//试探下一个作业序号
while (x[k]<n){
//检查x[k]序号的作业是否被重复处理
for(i=1;i<k;i++){
if(x[i]==x[k]) break;
}
if(i==k){
//计算此时机器1处理作业x[k]后的时间
sum1[k] = sum1[k-1]+a[x[k]];
//计算此时机器2处理作业x[k]后的时间
sum2[k] = Math.max(sum1[k],sum2[k-1])+b[x[k]];
//这里需要判断加入是否加入调度序列,条件为加入后的总时间小于BestTime
if(sum2[k]<BestTime) {
break;
}
x[k]++;//如果sum2[k]>BestTime,则试探下一个作业
}
//若x[k]序号的作业被重复处理,则处理下一个作业
else x[k]++;
}
if(x[k]<n&&k<n){
//处理第k+1个作业
k++;
}
else {
if(x[k]<n&&k==n) {
if (sum2[k] < BestTime) {
BestTime = sum2[k];
System.out.println("目前的最短时间安排是:");
for (int j = 1; j <= n; j++) {
System.out.print((x[j] + 1) + "->");
}
System.out.println("最短时间是:" + BestTime);
}
}
//回溯,在求出一个可能解后进行回溯,判断其他解情况的最小时间是否小于前面可能解的BestTime
x[k]=-1;
k--;
}
}
return BestTime;
}
总结:
回溯法的模板
while(true){ 结果数组++; while(当前节点k<节点数){ if(解成立) break; else 搜索下一个节点 } if(递归终止的条件){ 输出结果数组 } if(节点k可加入可能解){ 搜索k的子节点 }else{ 回溯,其于数组初始化=0(如x[k]=0),将k--; } }
动态规划
dp将待求解问题分解成若干个相互重叠的子问题,每个子问题对应决策过程的一个阶段,且子问题之间是重叠关系
动态规划的求解过程由以下三个阶段组成
- 划分子问题,子问题之间具有重叠关系
- 确定动态规划函数,根据子问题之间的重叠关系找到子问题满足的动态规划函数
- 填写表格,以自底向上的方式计算各个子问题的解并填表
数塔问题
【问题】:
如下图是一个数塔,从顶部出发在每一个节点可以选择向左或者向右走,一直走到底层,要求找出一条路径,使得路径上的数字之和最大.
想法:
子问题的划分,显然从顶层的数字出发,选择向左还是向右取决于下一层两个数的大小,故子问题之间具有重叠的特性
动态规划函数:动态规划的求解需要从底层开始决决策
最底层:maxAdd[n-1][i] = d[n-1][i]
0~n-1层:maxAdd[i][j]=max(maxAdd[i+1][j]+d[i][j],maxAdd[i+1][j+1]+d[i][j])+d[i][j];
path[i][j]表示i层的第j个数决策i+1层的选择路径
填写表格:即填写maxAdd[i][j]
public static void main(String[] args) {
int[][] d = {
{
8,0,0,0,0},{
12,15,0,0,0},{
3,9,6,0,0},{
8,10,5,12,0},{
16,4,18,10,9}};
System.out.println("路径上的数值和最大为:"+DataTower(d,5));
}
static int DataTower(int[][] d,int n){
//maxAdd数组用来存储动态规划的每一步的决策结果,注意这里动态规划是从底层开始
int[][] maxAdd = new int[n][n];
//path[i][j]用来表示每一次决策所选择的数组在数组d[n][n]中的列下标
int[][] path = new int[n][n];
//初始化maxAdd数组的底层
for (int i =0;i<n;i++){
maxAdd[n-1][i] = d[n-1][i];
}
//进行第i层的决策
for(int i = n-2;i>=0;i--){
//得出第i层的每一列的决策结果
for(int j=0;j<=i;j++){
if(maxAdd[i+1][j]>maxAdd[i+1][j+1]){
path[i][j]=j;
maxAdd[i][j]=maxAdd[i+1][j]+d[i][j];
}else {
path[i][j]=j+1;
maxAdd[i][j]=maxAdd[i+1][j+1]+d[i][j];
}
}
}
//从d[0][0]开始取数
System.out.printf("路径为:%d",d[0][0]);
//第1行的决策结果在path[0][0]中
int j = path[0][0];
for(int i=1;i<n;i++){
System.out.printf("-->%d",d[i][j]);
j = path[i][j];//本层决策是选择下一层列号为path[i][j]的数
}
return maxAdd[0][0];
}
图问题中的动态规划算法
多段图的最短路径问题
【问题】:
设图 G =(V,E)是一个带权有向图,如果把顶点集合 V 划分成 k 个互不相交的子集 Vi(2<=k<=n,1<=i<=k),使得 E 中的任何一条边 <u,v>,必有 u∈Vi, v∈Vi + m(1<=i<k, 1<i+m<=k),则称图 G 为多段图,称 s∈V1 为源点,t∈Vk 为终点。多段图的最短路径问题为从源点到终点的最小代价路径。
想法:
子问题的划分:顶点1到10的距离取决于1到8和9的距离,然后以此类推,故分解的子问题之间都由重叠的特性
动态规划函数:c(s,v)表示多段图的有向边<u,v>的权值,将从源点s到终点t的最短路径记为d(s,t),考虑原问题的部分解d(s,v)
d(s,v)=c(s,v)
d(s,v)=min{d(s,u)+c(u,v)}
填写表格,cost[i]表示从源点s到i需要的代价,path[j]表示从源点s到j的路径上j的前一个顶点
static int MAX = 10000;
public static void main(String[] args) {
//顶点数,边数
int vnum,arcnum;
Scanner sr = new Scanner(System.in);
vnum = sr.nextInt();//输入顶点数
arcnum = sr.nextInt();//输入边数
int[][] arc = new int[vnum][vnum];
//初始化图的代价矩阵
for(int i = 0; i<vnum;i++){
for(int j =0;j<vnum;j++){
arc[i][j]=MAX;
}
}
for(int k = 0;k<arcnum;k++){
int i =sr.nextInt();
int j = sr.nextInt();
int weight = sr.nextInt();
arc[i][j] = weight;
}
//打印arc数组
for(int i =0;i<vnum;i++){
for (int j =0;j<vnum;j++){
System.out.print(arc[i][j]+",");
}
System.out.println();
}
System.out.println("最小代价为:"+BackPath(vnum,arc));
}
public static int BackPath(int n,int[][] arc){
//cost[i]表示从源点s到i需要的代价
int[] cost = new int[n];
//path[j]表示从源点s到j的路径上j的前一个顶点
int[] path = new int[n];
//初始化cost,path数组
for(int i = 0 ;i < n ;i++){
cost[i]=MAX;
path[i]=-1;
}
//到源点的代价为0
cost[0]=0;
path[0]=-1;
for(int j=1;j<n;j++){
//查找j的入边
for(int i=j-1;i>=0;i--){
//如果源点s到j的代价>从源点s到i+arc[i][j],则将cost[j]替换,并且path[j]替换为i
if(cost[j]>arc[i][j]+cost[i]){
cost[j]=arc[i][j]+cost[i];
path[j]=i;
}
}
}
//输出终点
System.out.printf("路径为:"+(n-1));
int i = n-1;
while (path[i]>=0){
System.out.printf("<-%d",path[i]);
i=path[i];//求路径上顶点i的前一个顶点
}
System.out.println();
return cost[n-1];
}
多源点最短路径问题(Floyd)
【题目】:
给定带权有向图G=(V,E),对任意顶点v(i)、v(j),求顶点v(i)到顶点v(j)的最短路径
弗洛伊德算法 VS 迪杰斯特拉算法:迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。
子问题划分:顶点i到顶点j的最短路径依赖于各自到中间结点的距离,故子问题之间具有重叠的特征
动态规划函数:
如果dist[i][j]>dist[i][k]+dist[k][j],则dist[i][j]=dist[i][k]+dist[k][j];
填写表格,dist[i][j]用来表示从顶点i到顶点j的最短路径长度本题还可添加一个path[i][j]数组,用来记录从顶点i到顶点j的最短路径上顶点j的前一个顶点
static int N = 65536;
public static void main(String[] args) {
int n = 7;
int[][] arc = {
{
0, 5, 7, N, N, N, 2 },{
5, 0, N, 9, N, N, 3 },{
7, N, 0, N, 8, N, N },{
N, 9, N, 0, N, 4, N },{
N, N, 8, N, 0, 5, 4 },{
N, N, N, 4, 5, 0, 6 },{
2, 3, N, N, 4, 6, 0 }};
Floyd(arc,n);
}
public static void Floyd(int[][] arc,int n){
//dist[i][j]用来表示从顶点i到顶点j的最短路径长度
int[][] dist = new int[n][n];
//初始化dist数组
for(int i =0;i<n;i++){
for (int j =0;j<n;j++){
dist[i][j] = arc[i][j];
}
}
//遍历中间节点
for(int k =0;k<n;k++){
//遍历出发节点
for(int i=0;i<n;i++){
//遍历终点
for (int j=0;j<n;j++){
//不断对比,直至确定最短路径
if(dist[i][j]>dist[i][k]+dist[k][j]){
dist[i][j]=dist[i][k]+dist[k][j];
}
}
}
}
for (int i = 0;i<n;i++){
for (int j=0;j<n;j++){
System.out.print(dist[i][j]+",");
}
System.out.println();
}
}
TSP问题
组合问题中的动态规划算法
最长递增子序列问题
【问题】:
LIS(Longest Increasing Subsequence,最长递增子序列):给出一个序 列a1,a2,a3,a4,a5,a6,a7…an,求它的一个子序列(设为s1,s2,…sn),使得这个 子序列满足这样的性质,s1<s2<s3<…<sn并且这个子序列的长度最长。 输出这个最长子序列的长度。
【样例输入】 1 7 3 5 9 4 8 【样例输出】 长度为4【序列为1 3 5 9或1 3 4 8 】
定义数组L[i]表示元素序列a[0]–a[i]的最长递增子序列大的长度,x[i][n]存储a[0]–a[i]的最长公共子序列
- 划分子问题:在从a[0]开始遍历的过程中,后面序列的递增子序列长度以及序号依赖于前面的序列,故每个子问题之间都有重叠的特征
- 动态规划函数:
- i=0或a[j]>=a[i],L[i]=1
- L[j]+1>max&&a[j]<a[i],L[i]=L[j]+1;
- 填写表格即填写数组x,L
public static void main(String[] args) {
int[] a = {
5,2,8,6,3,6,9,7};
IncreaseOrder(a,8);
}
public static void IncreaseOrder(int[] a,int n){
int[] L =new int[10];//假设最多10个元素,L[i]表示元素序列a[0]--a[i]的最长递增子序列大的长度
int[][] x = new int[10][10];//x[i][n]存储a[0]--a[i]的最长公共子序列
//初始化,最长递增子序列为1
for(int i=0;i<n;i++){
L[i]=1;
x[i][0]=a[i];
}
//查找a[i]前的子序列
for(int i=1;i<n;i++){
int max=1;
for(int j=i-1;j>=0;j--){
//如果a[j]<a[i]并且L[j]+1>max,则将a[j]的最长公共子序列给到a[i],并且L[i]+1
if(L[j]+1>max&&a[j]<a[i]){
L[i]=L[j]+1;
max=L[i];
for(int k=0;k<max-1;k++){
//将a[j]的最长公共子序列给到a[i]
x[i][k]=x[j][k];
}
//在a[i]的公共子序列末尾补上当前a[i]
x[i][max-1]=a[i];
}
}
}
//求最长公共子序列的长度即所在序列的下标
int maxLength=L[0];
int index =0;
for(int i=1;i<n;i++){
if(maxLength<L[i])
{
maxLength=L[i];
index=i;
}
}
System.out.println("最长公共子序列的长度为:"+maxLength);
System.out.println(Arrays.toString(x[index]));
}
最长公共子序列问题
【题目】:
字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列<i0,i1,…,ik-1>,使得对所有的j=0,1,…,k-1,有xij=yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。
想法:
L[i][j]表示子序列x[i]和y[j]的最长公共子序列长度,S[i][j]则使用来表示在不同条件下,L[i][j]如何取值,并且在取值完成后能进行回溯,找到对应相等的序列
- 划分子问题:从i=1,j=1开始最每个L[i][j]寻找最长公共子序列,而每个最长公共子序列的长度又依赖于前面的L[i][j-1]或L[i-1][j],故子问题之间具有重叠的特征
- 动态规划函数:
- 如果x[i]==y[j],则L[i][j]=L[i-1][j-1]+1;
- 如果L[i][j-1]>L[i-1][j],则L[i][j]=L[i][j-1];
- 否则L[i][j]=L[i-1][j];
- 填写表格,即填写L,S
public static void main(String[] args) {
char[] x ={
' ','a','b','c','b','d','b'};
char[] y ={
' ','a','c','b','b','a','b','d','b','b'};
char[] z =new char[20];
CommonOrder(x,6,y,9,z);
}
public static void CommonOrder(char[] x,int m,char[] y,int n,char[] z){
//L[i][j]表示子序列x[i]和y[j]的最长公共子序列长度
int[][] L = new int[m+1][n+1];
int[][] S = new int[m+1][n+1];
//初始化L数组的第0行和第0列,这里初始话是为下面的求最长公共子序列创造条件
for(int i=0;i<=m;i++){
L[i][0]=0;
}
for(int i=1;i<=n;i++){
L[0][i]=0;
}
for(int i=1;i<=m;i++){
for (int j=1;j<=n;j++){
if(x[i]==y[j]){
L[i][j]=L[i-1][j-1]+1;
S[i][j]=1;//记录x[i]==y[j]相等的状态
}
else if(L[i][j-1]>L[i-1][j]){
L[i][j]=L[i][j-1];
S[i][j]=2;//记录i指向的x序列的最大公共子序列长度大于当前j指向的y序列的最大公共子序列长度
}else {
L[i][j]=L[i-1][j];//记录i指向的x序列的最大公共子序列长度小于当前j指向的y序列的最大公共子序列长度
S[i][j]=3;
}
}
}
int i =m,j=n;
int k =L[m][n];
while (i>0&&j>0){
if(S[i][j]==1){
z[k]=x[i];
i--;
j--;
k--;
}else if(S[i][j]==2){
j--;
}else {
i--;
}
}
System.out.println("序列为:"+Arrays.toString(z));
System.out.println("最长公共子序列长度为:"+L[m][n]);
}
0/1背包问题
【问题】:
给定n个重量为w[i] ,价值为v [i]的物品和容量为C CC的背包,求这个物品中一个最有价值的子集,使得在满足背包的容量的前提下,包内的总价值最大
想法:
数组V[i][j]表示前i个物品装入容量为j的背包的最大价值,数组x存储放入背包的序号,x[i]=1表示物品i放入背包中,x[i]=0表示物品i不放入背包中
- 划分子问题:从i=1,j=1开始确定V[i][j],而之后的每一次确定的背包可以装下的最大价值都与V[i-1][j]的值相关,故子问题之间具有重叠的特征
- 动态规划函数:
- 如果j<w[i],V[i][j]=V[i-1][j];
- 否则V[i][j]=Math.max(V[i-1][j],V[i-1][j-w[i]]+v[i]);
- 填写表格,即填写V,x
public static void main(String[] args) {
int[] w={
0,2,2,6,5,4};
int[] v={
0,6,3,5,4,6};
KnapSack(w,v,5,10);
}
public static void KnapSack(int[] w,int[] v,int n,int C){
//数组V[i][j]表示前i个物品装入容量为j的背包的最大价值
int[][] V = new int[n+1][C+1];
//初始话第0行第0列
for(int i=0;i<=n;i++){
V[i][0]=0;
}
for(int j =0 ; j<=C;j++){
V[0][j]=0;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=C;j++){
if(j<w[i]){
V[i][j]=V[i-1][j];
}else {
//计算前i个物品装入容量为j的背包的最大价值
V[i][j]=Math.max(V[i-1][j],V[i-1][j-w[i]]+v[i]);
}
}
}
//存储放入背包的序号,x[i]=1表示物品i放入背包中,x[i]=0表示物品i不放入背包中
int[] x = new int[n+1];
//求装入背包的物品
for(int i=n,j=C;i>0;i--){
if(V[i-1][j]<V[i][j]){
x[i]=1;
j=j-w[i];
}else {
x[i]=0;
}
}
System.out.print("放入物品为:");
for (int i=1;i<=n;i++){
if(x[i]==1){
System.out.print(i+" ");
}
}
System.out.println("背包最大价值为:"+V[n][C]);
}
闫氏DP分析法
0/1背包问题
完全背包问题
每个物品放入次数不限
石子划分
【题目】:
设有 NN 堆石子排成一排,其编号为 1,2,3,…,N1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 NN 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 44 堆石子分别为 1 3 5 2
, 我们可以先合并 1、21、2 堆,代价为 44,得到 4 5 2
, 又合并 1,21,2 堆,代价为 99,得到 9 2
,再合并得到 1111,总代价为 4+9+11=244+9+11=24;
如果第二步是先合并 2,32,3 堆,则代价为 77,得到 4 7
,最后一次合并代价为 1111,总代价为 4+7+11=224+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
【闫氏dp】:
查找问题中的动态规划法
最优二叉查找树
难
https://www.cnblogs.com/lpshou/archive/2012/04/26/2470914.html
【问题】:
n个键{a1,a2,a3…an},其相应的查找概率为{p1,p2,p3…pn}。构成最优BST,表示为T1n ,求这棵树的平均查找次数C[1, n](耗费最低)。换言之,如何构造这棵最优BST,使得C[1, n] 最小。
想法:
将问题分成多个阶段,逐段推进计算,后继实例解由其直接前趋实例解计算得到。对于最优BST问题,利用减一技术和最优性原则,如果前n-1个节点构成最优BST,加入一个节点an 后要求构成规模n的最优BST。按 n-1, n-2 , … , 2, 1 递归,问题可解。自底向上计算:C[1, 2]→C[1, 3] →… →C[1, n]。为不失一般性用
C[i, j] 表示由{a1,a2,a3…an}构成的BST的耗费。其中1≤i ≤j ≤n。这棵树表示为Tij。从中选择一个键ak作根节点,它的左子树为T[i][k-1],右子树为T[k+1][j]。要求选择的k 使得整棵树的平均查找次数C[i, j]最小。左右子树递归执行此过程。(根的生成过程)
动态规划函数:
- 填表,这里填写C(C[i][j]表示从节点i到节点j的这颗二叉树的平均查找次数),R(R[i][j]表示从节点i到节点j的这颗二叉树的根节点)
public static void main(String[] args) {
double[] p ={
0.1,0.2,0.4,0.3};
OptimalBST(p,4);
}
public static void OptimalBST(double[] p,int n){
//C[i][j]表示从节点i到节点j的这颗二叉树的平均查找次数
double[][] C = new double[n+2][n+2];
//R[i][j]表示从节点i到节点j的这颗二叉树的根节点
double[][] R = new double[n+1][n+1];
//初始化主对角线和次对角线
for(int i=1;i<=n;i++){
C[i][i-1]=0;
C[i][i]=p[i-1];
R[i][i]=i;
}
C[n+1][n]=0;
int j,mink;
double sum,min;
//d在这里充当增量即i与j的差,即区间长度
for(int d=1;d<n;d++){
for(int i=1;i<=n-d;i++){
j=i+d;
min=10000;
mink=i;
sum=0;
for(int k=i;k<=j;k++){
//计算i到j的节点概率和
sum=sum+p[k-1];
if(C[i][k-1]+C[k+1][j]<min){
min=C[i][k-1]+C[k+1][j];
mink = k;
}
}
//i到j的概率和+左右子树的平均查找次数
C[i][j]=min+sum;
R[i][j]=mink;
}
}
System.out.println("最少平均比较次数为:"+C[1][n]);
}
近似串匹配问题
【题目】:
问题:错误拼写检查,将字符串T修改为P,并且要求改动次数最少
例子:求出将"hsppay"(T)改成”happy“§的最少改动次数
想法:
- 将字符串P、T从i=1,j=1开始遍历,每次得到的D[i][j]都与之前的最少改动次数有关
- 动态规划函数
- 如果P[i-1]=T[j-1],则D[i][j] = min(D[i-1][j-1],D[i-1][j],D[i][j-1]);
注意这里pi=tj时包含三种情况,因为要求的是最小差别数故要取min:
- pi与tj一一对应,此时D[i][j]=D[i-1][j-1]
- pi多余,此时D[i][j]=D[i-1][j]
- tj多余,此时D[i][j]=D[i][j-1]
- 否则D[i][j] = min(D[i-1][j-1]+1,D[i-1][j]+1,D[i][j-1]+1)这里的情况与上面的相同
- 填表,D[i][j]表示表示到P到下标i的字符串与T到下标j的字符串的最小差别数
public static void main(String[] args) {
char[] P ={
'h','a','p','p','y'};
char[] T ={
'h','s','p','p','a','y'};
ASM(P,5,T,6);
}
public static void ASM(char[] P,int m,char[] T,int n){
//D[i][j]表示表示到P到下标i的字符串与T到下标j的字符串的最小差别数
int[][] D = new int[m+1][n+1];
//初始化第0行第0列
for(int i=0;i<=m;i++){
D[0][i]=i;
}
for(int j=1;j<=n;j++){
D[0][j]=j;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(P[i-1]==T[j-1]){
D[i][j] = min(D[i-1][j-1],D[i-1][j],D[i][j-1]);
}else {
D[i][j] = min(D[i-1][j-1]+1,D[i-1][j]+1,D[i][j-1]+1);
}
}
}
System.out.println("两字符串的最小差别数为"+D[m][n]);
}
//比较三个数的大小,输出最小值
public static int min(int a,int b,int c){
int min =Math.min(a,b);
if(c<min){
min =c;
}
return min;
}