ACM课程总结
一、课程内容
1.贪心算法
(1).贪心算法的定义:
在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。
(2).贪心算法的步骤
首先要确定一个可行的贪心标准,然后根据贪心标准进行排序处理(要学会使用stl中的排序函数),根据题意进行选择,保证达到题目要求。这就是贪心算法的一般要求,当然具体问题要具体分析,毕竟题目是不同的啊
(3).经典问题
活动安排问题 背包问题 最优装载问题 删数问题 多处最优服务次序问题 钓鱼选择问题(当然这些都是老师讲的,在题目中还有很多种情况,我也不可能全写一遍,这些都是比较经典的,呵呵)
(4).例题
①一层楼沿着走廊南北向的两边各有200个房间。最近,公司要做一次装修,需要在各个办公室之间搬运办公桌。由于走廊狭窄,办公桌都很大,走廊里一次只能通过一张办公桌。必须制定计划提高搬运效率。经理制定如下计划:一张办公桌从一个房间移到另一个房间最多用十分钟。当从房间i移动一张办公桌到房间j,两个办公室之间的走廊都会被占用。所以,每10分钟内,只要不是同一段走廊,都可以在房间之间移动办公桌。
代码:
# include<iostream>
# include<string.h>
using namespace std;
int main()
{
int n,m,f,s;
int move[200];
cin>>n;
while(n--)
{
memset(move,0,sizeof(move));
cin>>m;
for(int i=0;i<m;i++)
{
cin>>f>>s;
f=(f-1)/2;
s=(s-1)/2;
if(f>s){swap(f,s);}
for(int j=f;j<=s;j++)
{
move[j]++;
}
}
int max=0;
for(int i=0;i<200;i++)
{
if(move[i]>max)
max=move[i];
}
cout<<max*10<<endl;
}
return 0;
}
②设有n 个顾客同时等待一项服务。顾客i需要的服务时间为t i,1≤i≤n。共有s处可以提供此项服务。应如何安排n 个顾客的服务次序才能使平均等待时间达到最小?平均等待时间是n 个顾客等待服务时间的总和除以n。
对于给定的n 个顾客需要的服务时间和s的值,计算最优服务次序。
输入
输入数据的第一行有2 个正整数n (n≤10000)和s(s≤1000),表示有n 个顾客且有s 处可以提供顾客需要的服务。接下来的1 行中,有n个正整数,表示n个顾客需要的服务时间。
输出
输出数据只有一个整数(计算结果四舍五入),表示计算出的最小平均等待时间。
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
int main()
{
int i,n,j,k,minx;
int s;
double t;
int a[10005],b[1005];
while(cin>>n>>s)
{
for(i=0;i<n;i++)
cin>>a[i];
sort(a,a+n);
memset(b,0,sizeof(b));
for(i=0;i<n;i++)
{
minx=0x7fffffff;
k=0;
for(j=0;j<s;j++)
{
if(minx>b[j])
{
minx=b[j];
k=j;
}
}
b[k]+=a[i];
a[i]=b[k];
}
t=0;
for(i=0;i<n;i++)
t+=a[i];
t/=n;
printf("%d\n",(int)(t+0.5));
}
return 0;
}
2.搜索算法
1.搜索算法的定义
搜索算法是利用计算机的高性能来有目的地穷举一个问题的部分或所有的可能情况,从而求出问题的解的一种方法.相比于单纯的枚举算法有了一定的方向性和目标性.算法是在解的空间里,从一个状态转移(按照要求拓展)到其他状态,这样进行下去,将解的空间中的状态遍历,找到答案(目标的状态).
2.搜索的分类
(1.)广度优先搜索(BFS)
从初始状态S 开始,利用规则,生成所有可能的状态.构成的下一层节点,检查是否出现目标状态G,若未出现,就对该层所有状态节点,分别顺序利用规则.生成再下一层的所有状态节点,对这一层的所有状态节点检查是否出现G,若未出现,继续按上面思想生成再下一层的所有状态节点,这样一层一层往下展开。直到出现目标状态为止
(2.)深度优先搜索(DFS)
从初始状态,利用规则生成搜索树下一层任一个结点,检查是否出现目标状态,若未出现,以此状态利用规则生成再下一层任一个结点,再检查,重复过程一直到叶节点(即不能再生成新状态节点),当它仍不是目标状态时,回溯到上一层结果,取另一可能扩展搜索的分支。采用相同办法一直进行下去,直到找到目标状态为止。
3.搜索的框架
(1.)广度优先搜索(BFS)
While Not Queue.Empty ()
Begin
可加结束条件
Tmp = Queue.Top ()
从Tmp循环拓展下一个状态Next
If 状态Next合法 Then
Begin
生成新状态Next
Next.Step = Tmp.Step + 1
Queue.Pushback (Next)
End
Queue.Pop ()
End
(2.)深度优先搜索(DFS)【DFS分为递归和非递归两种】
递归
Function Dfs (Int Step, 当前状态)
Begin
可加结束条件
从当前状态循环拓展下一个状态Next
If 状态Next合法 Then
Dfs (Step + 1, Next ))
End
非递归
While Not Stack.Empty ()
Begin
Tmp = Stack.top()
从Tmp拓展下一个未拓展的状态Next
If 没有未拓展状态(到达叶节点) Then
Stack.pop()
Else If 状态Next合法 Then
Stack.push(Next)
End
4.例题
(1.)广度优先搜索(BFS)【一个代码比较简单的,能简单看出来的】
题意描述:一个人站在n点,只能向左走一步,或者向右走一步,或者直接走n的2倍步,要求走最少的步数抓住站在k点的牛。
题意分析:一看就是广搜的经典题目。一般要求最少步数用广搜。
#include<stdio.h>
#include<string.h>
#include<queue>
using namespace std;
#define MAX 200005
int v[MAX];
int cnt;
void BFS( int n, int k ){
queue<int>Q;
Q.push( n );
Q.push( cnt );
v[n] = 1;
while( !Q.empty() ){
n = Q.front();
Q.pop();
cnt = Q.front();
Q.pop();
if( n == k ){
return ;
}
if( n-1>=0 && !v[n-1] ){
Q.push( n-1 );
Q.push( cnt+1 );
v[n-1] = 1;
}
if( n+1<MAX && !v[n+1] ){
Q.push( n+1 );
Q.push( cnt+1 );
v[n+1] = 1;
}
if( 2*n<MAX && !v[2*n] ){
Q.push( 2*n );
Q.push( cnt+1 );
v[2*n] = 1;
}
}
}
int main(){
int n,k;
while( scanf( "%d%d", &n,&k ) != EOF ){
if( n >= k ){
printf( "%d\n",n-k );
continue;
}
cnt = 0;
memset( v, 0, sizeof(v) );
BFS( n, k );
printf( "%d\n", cnt );
}
return 0;
}
(2.)深度优先搜索(DFS)
poj 1154 : letters 给出一个roe*col的大写字母矩阵,一开始的位置为左上角,你可以向上下左右四个方向移动,并且不能移向曾经经过的字母。问最多可以经过几个字母。
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string.h>
using namespace std;
int visit[30];
int n,m;
int step[8] = {1,0,-1,0,0,1,0,-1};
int num[100][100],cou = 1,mmax = 0;;
void DFS(int i, int j)
{
for( int k=0; k<8; k+=2)
{
int a = i+step[k];
int b = j+step[k+1];
if( a>=1 && a<=n && b>=1 && b<=m && !visit[num[a][b]] )
{
visit[num[a][b]] = 1;
cou++;
DFS(a,b);
if( cou > mmax )
mmax = cou;
cou--;
visit[num[a][b]] = 0;
}
}
}
int main()
{
char str[100];
while( cin >> n >> m )
{
getchar();
memset(visit,0,sizeof(visit));
cou = 1; mmax = 1;
for(int i=1; i<=n; i++)
{
gets(str);
for(int k=0; k<m; k++)
num[i][k+1] = str[k] -'A' + 1;
}
visit[num[1][1]] = 1;
DFS(1,1);
cout << mmax << endl;
}
return 0;
}
3.动态规划
1.定义
动态规划是解决多阶段决策问题的一种方法,也是一种排除重复计算的算法,更具体的说,动态规划就是用空间换取时间。
2.动态规划问题具有以下基本特征:
(1)问题具有多阶段决策的特征。
(2)每一阶段都有相应的“状态”与之对应,描述状态的量称为“状态变量”。
(3)每一阶段都面临一个决策,选择不同的决策将会导致下一阶段不同的状态。
(4)每一阶段的最优解问题可以递归地归结为下一阶段各个可能状态的最优解问题,各子问题与原问题具有完全相同的结构。
3.动态规划的几个概念
阶段:据空间顺序或时间顺序对问题的求解划分阶段。
状态:描述事物的性质,不同事物有不同的性质,因而用不同的状态来刻画。对问题的求解状态的描述是分阶段的。
决策:根据题意要求,对每个阶段所做出的某种选择性操作。
状态转移方程:用数学公式描述与阶段相关的状态间的演变规律。
4.指导思想
在做每一步决策时,列出各种可能的局部解,依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。以每一步都是最优的来保证全局是最优的
5.动态规划问题的一般解题步骤
(1)判断问题是否具有最优子结构性质,若不具备则不能用动态规划。
(2)把问题分成若干个子问题(分阶段)。
(3)建立状态转移方程(递推公式)。
(4)找出边界条件。
(5)将已知边界值带入方程。
(6)递推求解。
注:动态规划的多阶段决策问题
如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策。多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果.其依靠的是最优性原理:不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。最优决策序列的子序列,一定是局部最优决策子序列。包含有非局部最优的决策子序列,一定不是最优决策序列。
6.例题:
(最长上升子序列问题)一个数的序列bi,当b1 < b2 < ... < bS 的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, ..., aN),我们可以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 <= i1 < i2 < ... <iK <= N。 比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8).你的任务,就是对于给定的序列,求出最长上升子序列的长度。
输入数据
输入的第一行是序列的长度N (1 <= N <= 1000)。第二行给出序列中的N 个整数,这些整数的取值范围都在0 到10000。
输出要求
最长上升子序列的长度。
输入样例
7
1 7 3 5 9 4 8
输出样例
4
代码:
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
int b[MAX_N + 10];
int aMaxLen[MAX_N + 10];
int main()
{
int i, j, N;
scanf("%d", & N);
for( i = 1;i <= N;i ++ )
scanf("%d", & b[i]);
aMaxLen[1] = 1;
for( i = 2; i <= N; i ++ )
{ //求以第i 个数为终点的最长上升子序列的长度
int nTmp = 0; //记录第i 个数左边子序列最大长度
for( j = 1; j < i; j ++ )
{ //搜索以第i 个数左边数为终点的最长上升子序列长度
if( b[i] > b[j] )
{
if( nTmp < aMaxLen[j] )
nTmp = aMaxLen[j];
}
}
aMaxLen[i] = nTmp + 1;
}
int nMax = -1;
for( i = 1;i <= N;i ++ )
if( nMax < aMaxLen[i])
nMax = aMaxLen[i];
printf("%d\n", nMax);
return 0;
}
经典问题(空间换时间)
楼梯问题
描述:(1)(NUAA 1350)某学校跳楼塔从底到顶只有一个楼梯,共 n 阶,一同学在第 k 次收到 CET-4 <= 250 的证书后欲爬上该塔自行了断,已知该同学每次可以上 1 阶或 2 阶,则他从塔底走到塔顶共有多少种方法?(2)当他爬到塔顶早已累得没有跳下去的勇气了,只想早点回到宿舍去上校内,此时他回城心切,可以每次下 1 阶、 2 阶或 3 阶,则他从塔顶走到塔底共有多少种方法?
输入:只有一个整数 n (1 <= n <= 30)
输出:只有 1 行,包含 2 个整数,中间用空格分开,分别为走上和走下 n 阶楼梯所对应的方法总数
样例输入:3
样例输出:3 4
问题分析
(1)上楼梯时
状态设计:f(i) 表示上 i 阶楼梯所对应的方法总数
求解目标:f(N)
状态转移方程:
f(i) = f(i-1) + f(i-2) 2 <= i <= N
f(i) = 1 0 <= i <= 1
(2)下楼梯时
状态设计:g(i) 表示下 i 阶楼梯所对应的方法总数
求解目标:g(N)
状态转移方程:
g(i) = g(i-1) + g(i-2) + g(i-3) 3 <= i <= N
g(i) = 2 i = 2
g(i) = 1 i = 1 或 i = 0
代码实现
初始化:
int n, i, f[31], g[31];
scanf("%d", &n);
(1)上楼梯时
f[1] = f[0] = 1;
for (i = 2; i <= n; ++i)
f[i] = f[i - 1] + f[i - 2];
(2)下楼梯时
g[1] = g[0] = 1;
g[2] = 2;
for (i = 3; i <= n; ++i)
g[i] = g[i - 1] + g[i - 2] + g[i - 3];
4.图算法
1.定义
①树(Tree):n(n≥0)个结点的有限集。若 n = 0,称为空树;若n>0,则它满足如下两个条件:
(1)有且仅有一个特定的称为根 (Root) 的结点;
(2) 其余结点可分为 m (m≥0) 个互不相交的有限集 T1, T2,T3, …, Tm,其中每一个集合本身又是一棵树,并称为 根的子树(SubTree)。
②图 (Graph) :一种复杂的非线性数据结构,由顶点集合及顶点间的关系(也称弧或边)集合组成。可以表示为:G=(V, {VR})其中 V 是顶点的有穷非空集合; VR 是顶点之间关系 的有穷集合,也叫做弧或边集合。弧是顶点的有序对,边是顶点的无序对。
③度:无向图中顶点 v 的度是和 v 相关联的边的数目,记为TD(v)。
入度:有向图中以顶点 v 为终点的弧数目称为 v 的入度,记ID(v)
出度:有向图中以顶点 v 为起点的弧数目称为 v 的出度,记OD(v)。
其中:TD(v) = ID(v) + OD(v)
④连通分量:无向图的极大连通子图;任何连通图的连通分量只有一个,即其本身;非连通图有多个连通分量(非连通图的每一个连通部分)。
强连通图:有向图G中,若对于V(G)中任意两个不同的顶点vi和vj,都存在从vi到vj以及从vj到vi的路径,则称G是强连通图。
强连通分量:有向图的极大强连通子图;任何强连通图的强连通分量只有一个,即其本身;非强连通图有多个强连通分量。
2.图的存储方式
①图的存储结构之数组表示法(邻接矩阵表示法)
对于一个具有n个顶点的图,可用两个数组存储。其中一个一维数组存储数据元素(顶点)的信息,另一个二维数组(图的邻接矩阵)存储数据元素之间的关系(边或弧)信息。
邻接矩阵:设 G = (V, {VR}) 是具有 n 个顶点的图,顶点的顺序依次为 {v1, v2, …, vn},则 G 的邻接矩阵是具有如下性质的 n 阶方阵:
特点:无向图的邻接矩阵对称,可压缩存储;有n个顶点的无向图需存储空间为 n(n-1)/2。
有向图邻接矩阵不一定对称;有n个顶点的有向图需存储空间为n²,空间复杂度O(n2),用于稀疏图时空间浪费严重。
无向图中顶点vi的度 TD(vi) 是邻接矩阵中第i行1的个数。
有向图中顶点vi的出度是邻接矩阵中第i行1的个数。有向图中顶点vi的入度是邻接矩阵中第i列1的个数。
邻接矩阵使用场合:
数据规模不大n <= 1000,m越大越好
稠密图最好用邻接矩阵
图中不能有多重边出现
②邻接表(类似于树的孩子链表表示法)
特点:若无向图中有n个顶点、e条边,则其邻接表需n个顶点表结点和2e个边表结点。适宜存储稀疏图。无向图中顶点 vi 的度为第 i 个单链表中的结点数。
邻接表使用场合:
顶点数很多n>1000,边数却不多。
采用邻接表存储后,很多算法的复杂度也都是跟边数有关。
连通性的问题很多情况边数不多,多采用邻接表存储方式
3.并查集
英文:Disjoint Set,即“不相交集合”
将编号分别为1…N的N个对象划分为不相交集合,
在每个集合中,选择其中某个元素代表所在集合。
常见两种操作:
合并两个集合
查找某元素属于哪个集合
4.具体问题
①最小生成树问题
生成树:由G的n-1条边构成的无环的子图,这些边的集合成为生成树。
最小生成树:所有生成树中权值最小的一个边集T为最小生成树,确定树T的问题成为最小生成树问题。
Prim算法
设G=(V,E)是连通带权图,V={1,2,…,n}。
构造G的最小生成树的Prim算法的基本思想是:首先置S={1},然后,只要S是V的真子集,就作如下的贪心选择:选取满足条件iÎS,jÎV-S,且c[i][j]最小的边,将顶点j添加到S中。这个过程一直进行到S=V时为止。
在这个过程中选取到的所有边恰好构成G的一棵最小生成树。
Kruskal算法:
将边按权值从小到大排序后逐个判断,如果当前的边加入以后不会产生环,那么就把当前边作为生成树的一条边。
最终得到的结果就是最小生成树。并查集
Kruskal算法步骤:
把原始图的N个节点看成N个独立子图
每次选取当前最短的边(前提操作是?),看两端是否属于不同的子图;若是,加入;否则,放弃;
循环操作该步骤二,直到有N-1条边
#include <stdio.h>
#include <iostream>
using namespace std;
#include <algorithm>
const int N=105;
int father[N];
int find(int x){
if(x!=father[x])
father[x]=find(father[x]);
return father[x];
}
struct edge{
int x,y,v;
}e[N*(N-1)/2];
int cmp(edge e1,edge e2){
return e1.v<e2.v;
}
int main(){
int n;
while(scanf("%d",&n)!=EOF&&n){
for(int i=0;i<=n;i++){
father[i]=i;
}
n=n*(n-1)/2;
for(int i=0;i<n;i++){
scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].v);
}
sort(e,e+n,cmp);
int res=0;
for(int i=0;i<n;i++){
int x=find(e[i].x);
int y=find(e[i].y)
if(x!=y) {
res+=e[i].v;
father[x]=y;
}
}
printf("%d\n",res);
}
return 0;
}
②单源点最短路径问题
问题描述:给定带权有向图G=(V, E)和源点v∈V,求从v到G中其余各顶点的最短路径。
Dijkstra算法
基本思想:设置一个集合S存放已经找到最短路径的顶点,S的初始状态只包含源点v,对vi∈V-S,假设从源点v到vi的有向边为最短路径。以后每求得一条最短路径v, …, vk,就将vk加入集合S中,并将路径v, …, vk , vi与原来的假设相比较,取路径长度较小者为最短路径。重复上述过程,直到集合V中全部顶点加入到集合S中。
Dijkstra算法——伪代码
1. 初始化数组dist、path和s;
2. while (s中的元素个数<n)
2.1 在dist[n]中求最小值,其下标为k;
2.2 输出dist[j]和path[j];
2.3 修改数组dist和path;
2.4 将顶点vk添加到数组s中;
计算单源最短路径问题的Dijkstra算法
#define NUM 100
#define maxint 10000
//顶点个数n,源点v,有向图的邻接矩阵为c
//数组dist保存从源点v到每个顶点的最短特殊路径长度
//数组prev保存每个顶点在最短特殊路径上的前一个结点
void dijkstra(int n, int v, int dist[], int prev[], int c[][NUM])
{
int i,j;
bool s[NUM]; //集合S
//初始化数组
for(i=1; i<=n; i++)
{
dist[i] = c[v][i];
s[i] = false;
if (dist[i]>maxint) prev[i] = 0;
else prev[i] = v;
}
//初始化源结点
dist[v] = 0;
s[v] = true;
//其余顶点
for(i=1; i<n; i++)
{
//在数组dist中寻找未处理结点的最小值
int tmp = maxint;
int u = v;
for(j=1; j<=n; j++)
if( !(s[j]) && (dist[j]<tmp))
{
u = j;
tmp = dist[j];
}
s[u] = 1; //结点u加入s中
//利用结点u更新数组dist
for(j=1; j<=n; j++)
if(!(s[j]) && c[u][j]<maxint)
{
//newdist为从源点到该点的最短特殊路径
int newdist = dist[u]+c[u][j];
if (newdist<dist[j])
{
//修正最短距离
dist[j] = newdist;
//记录j的前一个结点
prev[j] = u;
}
}
}
}
Bellman-Ford算法思想
Bellman-Ford算法构造一个最短路径长度数组序列dist 1 [u], dist 2 [u], …, dist n-1 [u]。其中:
dist 1 [u]为从源点v到终点u的只经过一条边的最短路径长度,并有dist 1 [u] =Edge[v][u];
dist 2 [u]为从源点v最多经过两条边到达终点u的最短路径长度;
dist 3 [u]为从源点v出发最多经过不构成负权值回路的三条边到达终点u的最短路径长度;
……
dist n-1 [u]为从源点v出发最多经过不构成负权值回路的n-1条边到达终点u的最短路径长度;
算法的最终目的是计算出dist n-1 [u],为源点v到顶点u的最短路径长度。
算法实现:
#define MAX_VER_NUM 10 //顶点个数最大值
#define MAX 1000000
int Edge[MAX_VER_NUM][MAX_VER_NUM]; //图的邻接矩阵
int vexnum; //顶点个数
void BellmanFord(int v) //假定图的邻接矩阵和顶点个数已经读进来了
{
int i, k, u;
for(i=0; i<vexnum; i++)
{
dist[i]=Edge[v][i]; //对dist[ ]初始化
if( i!=v && dis[i]<MAX ) path[i] = v; //对path[ ]初始化
else path[i] = -1;
}
for(k=2; k<vexnum; k++) //从dist1[u]递推出dist2[u], …,distn-1[u]
{
for(u=0; u< vexnum; u++)//修改每个顶点的dist[u]和path[u]
{
if( u != v )
{
for(i=0; i<vexnum; i++)//考虑其他每个顶点
{
if( Edge[i][u]<MAX &&
dist[u]>dist[i]+Edge[i][u] )
{
dist[u]=dist[i]+Edge[i][u];
path[u]=i;
}
}
}
}
}
}
二、感想
通过这半年的ACM学习,我了解了很多关于算法的知识,同时也掌握了一些基础的算法运用,老师曾说过,算法是编程中很重要的部分,这半年的学习我对这句话是充分认识啦,不管写什么程序,做题时都会下意识找算法,虽然很多题不会做但我还是感觉算法做题会省力,我想这也是为什么算法这么重要的原因。
半年之内我们学习了四个专题,从贪心算法和搜索内容到动态规划和图运算,内容越来越多,困难也越来越大,恐怕这就是ACM为什么这么难的原因吧,半年的学习虽然有了收获,但还是有不少不足,比如题目没完成,学习内容没完全掌握啦,总之不少啊,还是希望不要放下这门学科,希望能在暑假好好复习这四个专题!!!
方法总比困难多!!!