本文已参与「新人创作礼」活动,一起开启掘金创作之路。
算法设计与分析练习题
(一) 0-1背包问题
1. 问题描述
题目:
给定N个物品,每个物品有一个重量W和一个价值V.你有一个能装M重量的背包.问怎么装使得所装价值最大.每个物品只有一个.
输入格式
输入的第一行包含两个整数n, m,分别表示物品的个数和背包能装重量。
以后两行分别为每个物品的价值,重量
输出格式
输出2行,第一行包含一个整数,表示最大价值
第二行为装入背包的物品序号
样例输入
5 8
v[]={2,1,4,3,5};
w[]={1,4,2,3,5};
样例输出
装入背包中物品总价值最大为:
11
装入的物品的序号为:
1 3 5
数据规模和约定
1<=N<=200,M<=5000.
解题思路:
根据动态规划解题步骤(问题抽象化、建立模型、寻找约束条件、判断是否满足最优性原理、找大问题与小问题的递推关系式、填表、寻找解组成)找出01背包问题的最优解以及解组成,然后编写代码实现。
动态规划的原理:
动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。
Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积,定义V(i,j):当前背包容量 j,前 i 个物品最佳组合对应的价值,同时背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选)。
1、建立模型,即求max(V1X1+V2X2+…+VnXn);
2、寻找约束条件,W1X1+W2X2+…+WnXn<capacity;
3、寻找递推关系式,面对当前商品有两种可能性:
包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);
还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}。
其中V(i-1,j)表示不装,V(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i),但价值增加了v(i);
由此可以得出递推关系式:
j<w(i) V(i,j)=V(i-1,j)
j>=w(i) V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}
最优解回溯:
V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);
V(i,j)=V(i-1,j-w(i))+v(i)时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-w(i));
一直遍历到i=0结束为止,所有解的组成都会找到。
代码
#include<iostream>
#include <algorithm>
using namespace std;
int i,j;
int dp[30][100];
int item[100];
int value[100];
int weight[100];
int GetArray(int a[],int count)
{
for(int i=1;i<=count;i++)
cin>>a[i];
}
int GetMaxValue(int n,int m)
{
for(i=1;i<=n;i++)
{
for(j=1;j<=m;j++)
{
if(j<weight[i])
dp[i][j] = dp[i-1][j];
else
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}
}
}
int main()
{
int n,m;
cin>>n>>m;
GetArray(value,n);
GetArray(weight,n);
GetMaxValue(n,m);
cout<<"装入背包中物品总价值最大为:"<<endl;
cout<<dp[n][m]<<endl;
int b = m;
for(int i = n; i >= 1; i--){
if(dp[i][b] > dp[i - 1][b]){
item[i] = 1;
b -= weight[i];
}
}
cout<<"装入的物品的序号为:"<<endl;
for(int i = 1; i <= n; i++)
//cout << item[i] << " ";
if(item[i]==1)
cout<<i<<" ";
cout<<endl;
return 0;
}
(二)二维背包问题
1. 问题描述
题目
课后习题3-5:给定n种物品和一个背包。物品i的重量是wi,体积是bi,其价值是vi,背包的容量为C,容积为D。问:应该如何选择装入背包中的物品,使得装入背包中的物品的总价值最大?在选择装入背包额物品时,对每种物品i只有两种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。试设计一个解此问题的动态规划算法,并分析算法的计算复杂性。
输入格式
输入的第一行包含三个整数n, C ,D,分别表示物品的个数,背包能装重量和背包的容积。
以后三行分别为每个物品的价值,重量,体积
输出格式
输出2行,第1行,包含一个整数,表示最大价值。
第二行,装入背包的物品序号
样例输入
5 8 8
v[]={2,1,4,3,5};
w[]={1,4,2,3,5};
b[]={2,3,5,2,4};
样例输出
装入背包中物品总价值最大为:
8
装入的物品的序号为:
4 5
解题思路:
二维0-1背包问题是在0-1背包问题上加上一维
即给定n种物品和一个背包。物品i的重量是Wi,体积为Bi,其价值为Vi,背包的容量为C,容积为D.问:应该如何选择转入背包的物品,
使得物品总价最大。
首先我们分析二维0-1背包的最优子结构在,其实非常简单只要在在0-1背包的基础上扩充m将其添上一维k即可:
m[i][j][k],表示第i件物品放入容量为j,容积为k的背包时的最大价值。
对于第i件物品只有两种状况,放入与不放入。
接下来我们可以m(i,j,k)的计算公式
即可以放入时(j >= w k >= b) m(i,j,k) = max( m[i+1][j][k], m[i+1, j-w, k-b) + Vi )
不可放入时 m(i,j,k) = m[i+1][j][k]
第n个背包
m[n][j][k] 放入时m = v
不放入时 m = 0
代码:
package Practise915;
import java.util.Scanner;
public class Main {
public static void knapsack(int[] v,int[] w,int[] b,int c,int d,int[][][] m) {
int n = v.length-1;
int jMax = Math.min(w[n]-1,c);
int kMax = Math.min(b[n], d);
for(int j=0;j<=c;j++) {
for(int k=0;k<=d;k++) {
if(w[n]<=j && b[n]<=k) {
m[n][j][k]=v[n];
}
else {
m[n][j][k]=0;
}
}
}
for(int i=n-1;i>1;i--) {
for(int j=0;j<=c;j++)
for(int k=0;k<=d;k++) {
if (w[i]<=j && b[i]<=k)
m[i][j][k] = Math.max(m[i+1][j][k], m[i+1][j-w[i]][k-b[i]]+v[i]);
else
m[i][j][k] = m[i+1][j][k];
}
}
if(c>=w[1] && d>=b[1]){
m[1][c][d] = Math.max(m[2][c][d], m[2][c-w[1]][d-b[1]]+v[1]);
}
else {
m[1][c][d]=m[2][c][d];
}
}
public static void traceback(int[][][] m,int[] w,int c, int d,int[] x) {
int n = w.length-1;
for(int i=0;i<n;i++) {
if(m[i][c][d]==m[i+1][c][d]) {
x[i]=0;} //未装入
else { //装入
x[i]=1;
c -= w[i];
}
x[n]=(m[n][c][d]>0)?1:0;
}
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
int Weight = input.nextInt();
int Volume = input.nextInt();
int[] v = new int[n+1]; //价值
int[] w = new int[n+1]; //重量
int[] b = new int[n+1]; //体积
int[] x = new int[v.length];
int[][][] m = new int[v.length][Weight+1][Volume+1];
for (int i = 1;i <= n;i++) {
v[i] = input.nextInt();
}
for(int i=1;i<=n;i++) {
w[i] = input.nextInt();
}
for(int i=1;i<=n;i++) {
b[i] = input.nextInt();
}
knapsack(v,w,b,Weight,Volume,m);
traceback(m,w,Weight,Volume,x);
System.out.println("装入背包中物品总价值最大为:" + m[1][Weight][Volume]);
System.out.println("装入物品的序号为:");
for(int i=1;i<w.length;i++) {
if(x[i] == 1)
System.out.print(i + " ");
}
}
}
(三) 最长公共子序列
1. 问题描述
题目:
给定两个序列X={x1,x2,…,xm}和Y={y1,y2,…,yn},找出X和Y的最长公共子序列。
输入格式
输入两个整数n, m,分别表示两个序列中元素的个数;
依次输入两个序列中的所有元素值
输出格式
输出2行,第一行为最长公共子序列的长度。第二行为最长公共子序列的具体元素值
样例输入
8 9
1, 3, 4, 5, 6, 7, 7, 8
3, 5, 7, 4, 8, 6, 7, 8, 2
样例输出
5
3 4 6 7 8
解题思路
1.一个给定的序列的子序列,就是将给定序列中零个或多个元素去掉之后得到的结果。
给定串中任意个连续的字符组成的子序列称为该串的子串。
2.动态规划的基本思想就是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。
3.用c[i,j]表示Xi 和 Yj 的LCS的长度
递归公式为:
c[i][j]=0 if(i== 0||j==0)
c[i][j]=c[i-1][j-1]+1 if(i>0&&j>0&&x ==y)
c[i][j] = max{c[i-1][j],c[i][j-1]} if(i>0&&j>0&&x!=y)
代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int a[1001], b[1001];
int dp[1001][1001], len1, len2;
void lcs(int i, int j)
{
for(i=1; i<=len1; i++)
{
for(j=1; j<=len2; j++)
{
if(a[i-1] == b[j-1])
dp[i][j] = dp[i-1][j-1] + 1;
else if(dp[i-1][j] > dp[i][j-1])
dp[i][j] = dp[i-1][j];
else
dp[i][j] = dp[i][j-1];
}
}
}
void llcs()
{
int i, j, z = 0;
int c[1001];
memset(c, 0, sizeof(c));
i = len1, j = len2;
while(i!=0 && j!=0)
{
if(a[i-1] == b[j-1])
{
c[z++] = a[--i];
j--;
}
else if(dp[i-1][j] < dp[i][j-1])
j--;
else if(dp[i][j-1] <= dp[i-1][j])
i--;
}
for(i=z-1; i>=0; i--)
printf("%d ", c[i]);
printf("\n");
}
void GetArray(int arr1[],int n)//获取输入
{
for(int i=0;i<n-1;i++)
scanf("%d,",&arr1[i]);
scanf("%d",&arr1[n-1]);
}
int main()
{
int n,m;
cin>>n>>m;
GetArray(a,n);
GetArray(b,m);
len1=n;
len2=m;
lcs(len1,len2);
cout<<dp[n][m]<<endl;
llcs();
return 0;
}
(四)电路布线
1. 问题描述
题目
在电路板的上、下两端分别有n个接线柱。根据电路设计,用导线(i,π(i))将上端接线柱与下端接线柱相连,要求找到导线的最大不相交子集
示例
输入: 下端接线柱取值 [8,7,4,2,5,1,9,3,10,6]
输出: 最大不相交连线分别为:
3 4
5 5
7 9
9 10
最大不相交连线数目为:4
解题思路:
1.分析题目要求,采用动态规划的算法。这个问题符合最优子结构性质:问题的最优解包含子问题的最优解。
2.重叠子问题性质:子问题之间不独立的同时(这是区分分治算法的关键),少量子问题被重复解决了。子问题之间有联系的同时有些计算重复了,我们可以在计算第一次的时候将结果记录下来,以后再用到的时候,直接取值,不用再去花时间计算。
3.递归公式为: 与i连线的对应点 为n(i)
当i=1的时候:
size[i,j]=0 j<n(i)
size[i,j]=1 j>=n(i)
当i>1的时候:
size[i,j]=size[i-1,j] j<n(i)
size[i,j]=max{size[i-1,j],size[i-1,n(i)-1]+1} j>=n(i)
代码:
#include <iostream>
#include<stdio.h>
using namespace std;
const int N = 10;
void MNS(int C[],int n,int **size);
void Traceback(int C[],int **size,int n,int Net[],int& m);
int main()
{
int c[N+1];
int **size = new int *[N+1];
c[0]=0;
cout<<"下端接线柱取值为:"<<endl;
for(int i=1;i<N;i++)
scanf("%d,",&c[i]);
scanf("%d",&c[N]);
for(int i=0; i<=N; i++)
size[i] = new int[N+1];
MNS(c,N,size);
int Net[N],m;
Traceback(c,size,N,Net,m);
cout<<"最大不相交连线分别为:"<<endl;
for(int i=m-1; i>=0; i--)
cout<<Net[i]<<" "<<c[Net[i]]<<endl;
cout<<"最大不相交连线数目为:"<<size[N][N]<<endl;
return 0;
}
void MNS(int C[],int n,int **size)
{
for(int j=0;j<C[1];j++)
size[1][j]=0;
for(int j=C[1]; j<=n; j++)
size[1][j]=1;
for(int i=2; i<n; i++)
{
for(int j=0; j<C[i]; j++)
size[i][j]=size[i-1][j];//当i<c[i]的情形
for(int j=C[i]; j<=n; j++)
//当j>=c[i]时,考虑(i,c[i])是否属于MNS(i,j)的两种情况
size[i][j]=max(size[i-1][j],size[i-1][C[i]-1]+1);
}
size[n][n]=max(size[n-1][n],size[n-1][C[n]-1]+1);
}
void Traceback(int C[],int **size,int n,int Net[],int& m)
{
int j=n;
m=0;
for(int i=n;i>1;i--)
{
if(size[i][j]!=size[i-1][j])//此时,(i,c[i])是最大不相交子集的一条边
{
Net[m++]=i;
j=C[i]-1;//更新扩展连线柱区间
}
}
if(j>=C[1])//处理i=1的情形
Net[m++]=1;
}
(五)基础练习 数列排序
给定一个长度为n的数列,将这个数列按从小到大的顺序排列。1<=n<=200输入描述:
第一行为一个整数n。
第二行包含n个整数,为待排序的数,每个整数的绝对值小于10000。
输入样例:
5
8 3 6 4 9
输出描述:
输出一行,按从小到大的顺序输出排序后的数列。
输出样例:
3 4 6 8 9
代码:
#include <iostream>
#include<algorithm>
using namespace std;
int main()
{
int n;
cin >> n;
int array[n];
for (int i = 0; i < n; i++)
{
cin >> array[i];
}
sort(array, array + n);
for (int i = 0; i < n; i++)
{
cout << array[i] << " ";
}
}
(六)算法训练 数的划分
题目:
将整数n分成k份,且每份不能为空,任意两份不能相同(不考虑顺序)。
例如:n=7,k=3,下面三种分法被认为是相同的。
1,1,5; 1,5,1; 5,1,1;
问有多少种不同的分法。输入描述:
n,k
输入样例:
7 3
输出描述:
一个整数,即不同的分法
输出样例:
4 {四种分法为:1,1,5;1,2,4;1,3,3;2,2,3;}
代码:
#include<iostream>
#include<math.h>
#include<memory.h>
using namespace std;
int main()
{
int n, k;
cin >> n >> k;
int dp[240][15];
memset(dp,0, sizeof(dp));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
if (j == 1)
dp[i][j] = 1;
else
dp[i][j] = dp[i - j][j] + dp[i - 1][j - 1];
cout << dp[n][k] << endl;
return 0;
}
(七)基础练习 矩阵乘法**
题目:
给定一个N阶矩阵A,输出A的M次幂(M是非负整数)
例如:
A =
1 2
3 4
A的2次幂
7 10
15 22输入描述:
第一行是一个正整数N、M(1<=N<=30, 0<=M<=5),表示矩阵A的阶数和要求的幂数
接下来N行,每行N个绝对值不超过10的非负整数,描述矩阵A的值
输入样例:
2 2
1 2
3 4
输出描述:
输出共N行,每行N个整数,表示A的M次幂所对应的矩阵。相邻的数之间用一个空格隔开
输出样例:
7 10
15 22
代码:
#include<iostream>
#include<cstring>
using namespace std;
int start[50][105];
int tmp[50][105];
int last[50][105];
void Display(int a[][105], int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
cout << a[i][j] << " ";
cout << endl;
}
}
void Getmatrix(int start[][105], int tmp[][105], int last[][105], int n, int m)
{
for (int i = 1; i < m; i++)
{
for (int j = 0; j < n; j++)
{
for (int k = 0; k < n; k++)
{
int temp = 0;
for (int x = 0; x < n; x++)
{
temp += start[j][x] * tmp[x][k];
last[j][k] = temp;
}
}
}
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
tmp[i][j] = last[i][j];
}
}
}
}
int main()
{
int n, m;
cin >> n >> m;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
cin >> start[i][j];
}
}
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
tmp[i][j] = start[i][j];
}
}
if (m == 0)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (i == j)
last[i][j] = 1;
}
}
Display(last, n);
}
else if (m == 1)
{
Display(start, n);
}
else
{
Getmatrix(start, tmp, last, n, m);
Display(last, n);
}
return 0;
}