把这个学期学习的算法整理了一遍。
一、动态规划
1、最长公共子序列
#include <iostream>
#include <cstdio>
#include <string>
#define MAX 10001
using namespace std;
int x[MAX];
int y[MAX];
int c[MAX][MAX];
int max(int a, int b)
{
if (a > b) return a;
else return b;
}
int main()
{
int i;
int j;
int n;
int m;
cin >> n;
cin >> m;
for (i = 1; i <= n; i++)
cin >> x[i];
for (i = 1; i <= m; i++)
cin >> y[i];
for (i = 0; i <= n; i++)
{
for (j = 0; j <= n; j++)
{
if (i == 0 || j == 0)
{
c[i][j] = 0;
}
else if (x[i] == y[j])
{
c[i][j] = c[i - 1][j - 1] + 1;
}
else
{
c[i][j] = max(c[i][j - 1], c[i - 1][j]);
}
}
}
cout << c[n][n];
}
2、最大子段和
b[1]=max{a[1]};
b[2]=max{a[1]+a[2],
a[2]};
b[3]=max{a[1]+a[2]+a[3],
a[2]+a[3],
a[3]};
b[4]=max{a[1]+a[2]+a[3]+a[4],
a[2]+a[3]+a[4],
a[3]+a[4],
a[4]};
则最大子段和为:MAX { b[1] , b[2], b[3,]……., b[n] }
b[j] = max{b[j-1]+a[j], a[j]}, 1≤j ≤n
b[j]=a[j] // b[j-1]<0
b[j]=b[j-1]+a[j] // b[j-1]>=0
int MaxSum(int *a, int n)
{ int sum=0,b=0;
for( j=1; j<=n; j++)
{ if ( b>0) b+=a[j]; else b=a[j];
if(b>sum) sum= b; }
return sum;
}
3、找零钱问题
问题描述
现有种不同面值的货币,每种面值的货币可以使用任意张。顾客结账时,你需要找给顾客元零钱,你可以给出多少种方法。例如,有1、2、3元三种面值的货币,你需要找零3元,那么共有3种方法:
1 1 1
1 2
3
输入零钱数组 penny[n]
找零目标 aim
寻找有多少种方法来组成 aim (每种零钱可以使用无数张)
分析
设 dp[i][j] 为 前 i 种货币凑成目标 j 的总方法数
dp[i][j]=dp[i-1][j]+dp[i-1][j-penny[i]]+…dp[i-1][j-kpenny[i]]
k代表使用 penny[i] 的个数
其中 k 是保证j-kpenny[i]>=0 的最大值
可以继续推下去
设 x =j-penny[i]
则dp[i][x]=dp[i-1][x]+dp[i-1][x-penny[i]]+…dp[i-1][x-kpenny[i]]
所以 dp[i-1][j-penny[i]]+…dp[i-1][j-kpenny[i]]=dp[i][x]=dp[i][j-penny[i]]
得
dp[i][j]=dp[i-1][j]+dp[i][j-penny[i]]
边界条件为 j>=penny[i]
int core (int penny[], int n, int aim)
{
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= aim; j++) {
if (j < penny[i])
{
dp[i][j] = dp[i - 1][j];
}
else
{
dp[i][j] = dp[i - 1][j] + dp[i][j - penny[i]];
}
}
}
return dp[n][aim];
}
4、走方格(最小路径和)
有一个矩阵map,它每个格子有一个权值。从左上角的格子开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。
给定一个矩阵map及它的行数n和列数m,请返回最小路径和。
设dp[n][m]为走到n*m位置的路径长度,那么显而易见dp[n][m] = min(dp[n-1][m],dp[n][m-1]);
int core(int map[][MAX], int n, int m)
{
for(int i=1;i<n;i++){
for(int j=1;j<m;j++){
dp[i][j] = min(dp[i][j-1]+map[i][j],dp[i-1][j]+map[i][j]);
}
}
return dp[n-1][m-1];
}
5、最长上升子序列问题
给定一个数字序列a,求该序列中最长上升子序列的长度。例如a={1,4,2,5,3},其最长上升子序列为{1,2,3},因此最长上升子序列的长度为3
假设序列a长度为n,构建一个长度为n的一维数组dp[n],dp[i]代表了以a[i]结尾的上升子序列的长度。很显然,dp[i] = 1 + max{dp[j] | a [j] < a[i] 且 j < i } ,最长上升子序列的长度为max{dp[i]}
int core(int map[][MAX], int n, int m)
{
dp[1] = 1;
for (int i = 1; i <= n; i++)
{
int max = 0;
for (int j = 1; j < i; j++)
{
if (a[i] > a[j])
{
max = Math.max(max, dp[j]);
}
}
dp[i] = max + 1;
}
int result = 0;
for (int i = 1; i <= n; i++)
{
if (result < dp[i])
{
result = dp[i];
}
}
return result;
}
5、背包问题
1、01背包
一个背包有一定的承重c,有n件物品,每件都有自己的价值,记录在数组v中,也都有自己的重量,记录在数组w中,每件物品只能选择要装入背包还是不装入背包,要求在不超过背包承重的前提下,选出物品的总价值最大。
设dp[i][j] 为前 i 个物品用 j 的重量来装时可以放下的最大价值
dp[i][j]=max{dp[i-1][j-w[i]]+v[i],dp[i-1][j]}
前面的是放第i个物品,后面的是不放。
int core (int c, int n, int w[], int v[])
{
for (int i = 1; i <= n; i++)
{
dp[i][0] = 0;
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= c; j++)
{
if (j >= w[i]) //能放下
{
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
else
{
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][c];
}
空间优化
dp[i][j] 每次变化都与 dp[i-1][x] 有关
所以 得到新的状态转移方程
dp[j]=max(dp[j],dp[j-w[i]]+v[i])
其实数组每次写入一行数据都是根据上一行的数据进行写入,不需要用二维数组,可以用一维数组,每次根据自身进行更新即可,重点是[i-1][j - weight[i]]这个数据的意思是我要在表中上一行找比j小的数据,那换成一维的话,我找的是自身(只有一行,没有上一行)j左边的数据,这个数据必须保证是上一次(i-1)写入的,肯定不能从左向右写入数据,那样的话[i-1][j - weight[i]] 对应的就是这一次(i)写入的,上一次数据没有利用就丢失了,所以j循环应该从M到1,从右向左向一维数组写入数据。
for(int i = 1; i <= n; i++)
{
for (int j = c; j >= 1; j--)
{
if (j >= w[i])
{
dp[j] = Math.max(dp[j], dp[j -w[i]] + v[i]);
}
}
2、完全背包
for(int i=1; i<=n; i++)
for(int j=w[i]; j<=c; j++)//注意此处,与0-1背包不同,这里为顺序,0-1背包为逆序
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
二、贪心
1、活动安排问题
设有n个活动的集合E={1,2,…, n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。
每个活动 i 都有一个要求使用该资源的起始时间 si 和一个结束时间 fi ,且si < fi 。如果选择了活动i,则它在半开时间区间[si, fi)内占用资源。若区间[si, fi)与区间[sj, fj)不相交,则称活动 i与活动 j是相容的。也就是说,当si ≥ fj或sj ≥ fi时,活动 i与活动 j相容。
活动安排问题是,在所给的活动集合中选出最大的相容活动子集合。
void GreedySelector(int n, Type s[], Type f[], bool A[])
{ // s数组存储活动起始时间,f数组存储活动结束时间,且各活动按结束时间已按非减序排列;A数组存储被选中的活动。
A[1]=true; // 选择活动1(结束最早的活动)
int j=1; // j记录最近一次被选中的活动
for (int i=2;i<=n;i++) {
if (s[i]>=f[j]) { A[i]=true; j=i; }
else A[i]=false;
} // 每次总是选择具有最早完成时间的相容活动加入集合A中
}
三、回溯法
1、装载问题
有n个集装箱要装上2艘载重量分别为C1和C2的轮船。其中集装箱i的重量为Wi,且(W1+W2+….+Wn<=C1+C2)。
装载问题是,是否有一个合理装载方案,可将这n个集装箱都装上这2个轮船,若有,请给出解决方案。
#define num 100
int n,c1,c2,w[num];// n个集装箱,A,B货轮载重量分别为C1,C2,W[i],第i个集装箱的重量
int cw,bw,rw;//cw,当前集装箱货物重量;bw,最优载重重量,rw,剩余集装箱重量;
int x[num],bx[num];//x[],A货轮的当前结果;bx[],A货轮的最优结果;
void BackTrack(int i)
{
//处理完了前n个集装箱;
if(i>n){
if(cw>bw){//cw,目前A中装了cw重量的集装箱;
//更新最优解;
bw=cw;
for(int i=1;i<=n;i++) bx[i]=x[i];
}
return;
}
//rw表示处理完第i个之后(选或不选),还剩下rw-w[i]重量的集装箱未处理;
rw-=w[i];
if(cw+w[i]<=c1){//cw,第i个货箱之前的重量 + 第i个货箱小于A的最大重量C1;
cw+=w[i];//加上
x[i]=1;//标记i被选
BackTrack(i+1);
cw-=w[i];//减去重量
x[i]=0;//撤销标记;
}
//不选择第i个物品的话;
//if cw:表示[1:i)的数据 rw:表示(i,n]的数据 ,不包括第i个的数据
//如果不包括第i的数据的和(cw+rw) 大于 目前最优解bw,则可以递归下去;
//如果cw+rw<=bw 那么就算把剩下的全部装入也不可能得到更好的答案,所以省去。
if(cw+rw > bw){
x[i]=0;
BackTrack(i+1);
}
rw+=w[i];
return ;
}
int main()
{
//读入数据;
scanf("%d%d%d",&n,&c1,&c2);
for(int i=1;i<=n;i++)
{
scanf("%d",&w[i]);
rw+=w[i];//rw表示目前最优集装箱的剩余重量;
}
//递归回溯
BackTrack(1);
//bw表示A货轮装下的货物重量;剩余的重量 > B可以放下的最多,则不可;
if(rw-bw>c2){
printf("没有装载方案\n");
}else{
printf("货轮A:\n");
for(int i=1;i<=n;i++) if(bx[i]) printf("%d ",i);
printf("\n货轮B:\n");
for(int i=1;i<=n;i++) if(0==bx[i]) printf("%d ",i);
}
return 0;
}
2、批处理作业调度问题
给定 n 个作业的集合 j = {j1, j2, …, jn}。每一个作业 j[i] 都必须先由机器1处理,然后由机器2处理。作业 j[i] 需要机器 j 的处理时间为 t[j][i] ,其中i = 1, 2, …, n, j = 1, 2。对于一个确定的作业调度,设F[j][i]是作业 i 在机器 j 上的完成处理的时间。所有作业在机器2上完成处理的时间之和 称为该作业调度的完成时间之和。 批处理作业调度问题要求对于给定的 n 个作业,制定最佳作业调度方案,使其完成时间和达到最小。
int x[100],bestx[100],m[100][100];//m[j][i]表示在第i台机器上作业j的处理时间
//数组bestx记录相应的当前最佳作业调度。
int f1=0,f2,cf=0,bestf=10000,n; //bestf记录当前最小完成时间和
void swap(int *x,int t,int j)
{
int temp = x[t];
x[t] = x[j];
x[j] = temp;
}
void Backtrack(int t)
{
int tempf,j,i;
if(t>n) //到达叶子结点,搜索到最底部
{
for( i=1; i<=n; i++)
bestx[i]=x[i];
bestf=cf;
}
else //非叶子结点
{
for(j=t; j<=n; j++)
{
f1+=m[x[j]][1]; //记录作业在第一台机器上的完成处理时间
tempf=f2;//保存上一个作业在机器2的完成处理时间
f2=(f1>f2?f1:f2)+m[x[j]][2];//机器2开始作业的时间为机器1完成该作业的时间与机器2完成上一个作业的时间中较大的那一个。
cf+=f2; //cf记录当前在机器2上的完成时间和
if(cf<bestf) //贪心???
{
swap(x,t,j); //交换两个作业的位置
Backtrack(t+1);
swap(x,t,j);
}
f1-=m[x[j]][1];
cf-=f2;
f2=tempf;
}
}
}
int main()
{
int i,j;
printf("请输入作业数量\n");
scanf("%d",&n);
printf("请输入在各机器上的处理时间\n");
for(i=1; i<=2; i++)
for(j=1; j<=n; j++)
scanf("%d",&m[j][i]);
for(i=1; i<=n; i++)
x[i]=i; //记录当前调度
Backtrack(1);
printf("调度作业顺序\n");
for(i=1; i<=n; i++)
printf("%d\t",bestx[i]);
printf("\n");
printf("处理时间:\n");
printf("%d\n",bestf);
return 0;
}
3、图的m着色问题
给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。是否有一种着色法使G中每条边的2个顶点着不同颜色。这个问题是图的m可着色判定问题。
若一个图最少需要m种颜色才能使图中每条边连接的2个顶点着不同颜色,则称这个数m为该图的色数。求一个图的色数m的问题称为图的m可着色优化问题。
int m; //给定的颜色数
int a[n][n]; //n个顶点图的邻接矩阵
int x[n]; //存放着色方案
int flag=0; //是否M可着色
bool Ok(int k)
{ // 着色方案是否可行
for (int j=1;j<=n;j++)
if ((a[k][j]==1)&&(x[k]==x[j])) return false;
return true;
}
void backtrack(t) //判断图是否M可着色
{ if (t>n) //有着色方案
{ flag=1; return; }
for (i=1; i<=m; i++) //对M种颜色进行遍历
{ x[t]=i;
if ( ok(t)) backtrack(t+1) ;
}
return;
}
4、N皇后问题
在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。求N皇后问题的一种放法,或求N皇后问题的所有放法。
x[i]:i表示第i行皇后
x[i]表示第i行上皇后放第几列
不同行:数组x的下标保证不重复
不同列:x[i]!=x[j] (i<=I,j<=n;i!=j)
不同对角线:abs(x[i]-x[j])!=abs(i-j)
bool Place(int k)
{
for (int j=1;j<k-1;j++) //填到第K行时,就与前1~(K-1)行都进行比较
if ((abs(k-j)==abs(x[j]-x[k]))||(x[j]==x[k])) return false;
return true;
}
void Backtrack(int t)
{
if (t>n) sum++;
else
for (int i=1;i<=n;i++) { //每层均有n种放法
x[t]=i; //放置本层皇后
if (Place(t)) Backtrack(t+1);
}
}
5、最大子团问题
给定无向图G=(V,E)。如果UV,且对任意u,vU有(u,v)E,则称U是G的完全子图。G的完全子图U是G的团当且仅当U不包含在G的更大的完全子图中。G的最大团是指G中所含顶点数最多的团。
首先设最大团为一个空团,往其中加入一个顶点,然后依次考虑每个顶点,查看该顶点加入团之后仍然构成一个团。如果可以,考虑将该顶点加入团或者舍弃两种情况;如果不行, 直接舍弃,然后递归判断下一顶点。对于无连接或者直接舍弃两种情况,在递归前,可采用剪枝策略来避免无效搜索。
void Backtrack(int i) { // 计算最大团
if (i > n) { // 到达叶结点
for (int j = 1; j <= n; j++) bestx[j] = x[j];
bestn = cn; return;} //当前团的顶点数cn
int OK = 1;
for (int j = 1; j < i; j++) // 检查顶点 i 与当前团的连接
if (x[j] && a[i][j] == 0) { // i与j不相连
OK = 0; break; }
if (OK) { // 进入左子树
x[i] = 1; cn++;
Backtrack(i+1);
x[i] = 0; cn--;}
if (cn + n - i > bestn) { // 进入右子树
x[i] = 0;
Backtrack(i+1);}
}
四、分支限界法
1、单源最短路径
在有向图G中,每一边都有一个非负边权。要求图G的从源顶点s到目标顶点t之间的最短路径。
#include <iostream>
#include<queue>
using namespace std;
typedef struct ArcCell{
int adj;//保存权值
int info;//存储最短路径长度
}ArcCell,AdjMaxtrix[100][100];
typedef struct{
int data;
int length;
}VerType;
typedef struct{
VerType vexs[100];//顶点向量
AdjMaxtrix arcs;
int vexnum;//顶点数
int arcnum;//弧数
}Graph;
Graph G;
queue<int> q;
void CreateGraph()
{
int m,n,t;
printf("输入顶点数和弧数:");
scanf("%d%d",&G.vexnum,&G.arcnum);
printf("输入顶点:");
for(int i=1;i<=G.vexnum;i++)
{
scanf("%d",&G.vexs[i].data);
G.vexs[i].length=10000;
}
for(int i=1;i<=G.vexnum;i++)
for(int j=1;j<=G.vexnum;j++)
{
G.arcs[i][j].adj=0;
}
printf("输入弧及权重:\n");
for(int i=1;i<=G.arcnum;i++)
{
scanf("%d%d%d",&m,&n,&t);
G.arcs[m][n].adj=1;
G.arcs[m][n].info=t;
}
}
int NextAdj(int v,int w)
{
for(int i=w+1;i<=G.vexnum;i++)
if(G.arcs[v][i].adj)
return i;
return 0;//not found;
}
void ShortestPaths(int v)
{
int k=0;//从首个节点开始访问
int t;
G.vexs[v].length=0;
q.push(G.vexs[v].data);
while(!q.empty())
{
t=q.front();
k=NextAdj(t,k);
while(k!=0)
{
if(G.vexs[t].length+G.arcs[t][k].info<=G.vexs[k].length)//减枝操作
{
G.vexs[k].length=G.vexs[t].length+G.arcs[t][k].info;
q.push(G.vexs[k].data);
}
k=NextAdj(t,k);
}
q.pop();
}
}
int main()
{
CreateGraph();
ShortestPaths(1);
for(int i=1;i<=G.vexnum;i++)
printf("%d\n%d\n",G.vexs[i].data,G.vexs[i].length);
return 0;
}