ACM总结
这一学期,感觉学到的最多的就是这门课了。开始学这门课,想着就是多学习学习算法。
不过后来,我班上的选这门课的退了很多,都说很难。我没想太多。选了就选了,再难也要就走下去。
第一节课,费老师说的多年愿望,也挺高兴和感动的。我知道他的希望我是没法承担的,课太多了,注定不能全心去做。老师讲的很生动,没个专题都有大20多个题要做,时间一个月吧(看着很少,但有时一上午一个题也做不出来很正常)。在专题开始轻松,最后几天就忙成狗了,一直拖得缘故。
本学期,主要讲了4个专题,贪心,搜索,动态规划,图。
贪心专题
贪心算法
贪心法的基本思路:
——从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到某算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1. 不能保证求得的最后解是最佳的;
2. 不能用来求最大或最小解问题;
3. 只能求满足某些约束条件的可行解的范围。
实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;
但是该算法也存在一些问题:
· 不能保证求得的最后解释最佳的;
· 不能用来求最大或着最小解的问题;
· 只能求满足某些约束条件的可行解的范围。
一般的题型是:
N个商品,每个商品的重量为WI,价格为:PI,现有一个背包,最多能装M的重量.其中(0<=I< N,0< wi< M).
问:怎样装能使包中装入的商品价值最高(对于每个商品可以只装该商品的一部分)
伪代码:
参数分别为 n:物品数 M:背包最多能装的重量 v[]:价值数组 w[]重量数组
void Knapsack(int n,float M,float v[],float w[],float x[])
{
Sort(n,v,w); //进行排序int i;for (i = 1 ; i <= n ; i++)
x[i] = 0;
float c=M; //C为背包剩余空间
for (i=1;i<=n;i++) {
if (w[i] > c) break;
x[i]=1; //选取第i个物品
c-=w[i]; //剩余重量减少
}if (i <= n) //当此时背包的容量不够存放整个物品的情况时,存放一部分
x[i]=c / w[i];
}
· 1
· 2
· 3
· 4
· 5
· 6
· 7
· 8
· 9
· 10
· 11
· 12
· 13
· 14
· 15
贪心算法一般在开始策略前会进行排序,排序后再进行最优化选择。排序一般直接使用sort函数,在学这门课之前,我写排序还是用最基本版的冒泡或者简单选择。而又学了数据结构,排序的方法就很多了。但是一般我还是会用这个排序,毕竟简单嘛。还有一种很好用的容器是优先队列,这个直接放进数去就可以自动排序,更好用。
图的应用中最小生成树的算法都是很好的贪心算法。事实上贪心算法可以和其他很多算法结合起来,更好用,结果也更准确。
背包问题
在0 / 1背包问题中,需对容量为c 的背包进行装载。从n 个物品中选取装入背包的物品,每件物品i 的重量为wi ,价值为pi 。对于可行的背包装载,背包中物品的总重量不能超过背包的容量,最佳装载是指所装入的物品价值最高,即n åi=1pi xi 取得最大值。约束条件为n åi =1wi xi≤c 和xiÎ[ 0 , 1 ] ( 1≤i≤n)。
搜索专题
什么是搜索算法?搜索算法是利用计算机的高性能来有目的地穷举一个问题的部分或所有的可能情况,从而求出问题的解的一种方法。相比于单纯的枚举算法有了一定的方向性和目标性。算法是在解的空间里,从一个状态转移(按照要求拓展)到其他状态,这样进行下去,将解的空间中的状态遍历,找到答案(目标的状态)。
什么是状态、状态转移?状态(state)是对问题在某一时刻进展情况的数学描述,或者是数学抽象。每一个状态都会是答案的一个“可能的”解。状态的转移就是问题从一个状态转移到另一个状态,这样就可以进行搜索的一步步延伸,最后要得到的解也是其中的一个状态。
如何进行状态转移
A广度优先搜索(BFS)
基本思想:从初始状态S 开始,利用规则,生成所有可能的状态。构成的下一层节点,检查是否出现目标状态G,若未出现,就对该层所有状态节点,分别顺序利用规则。
生成再下一层的所有状态节点,对这一层的所有状态节点检查是否出现G,若未出现,继续按上面思想生成再下一层的所有状态节点,这样一层一层往下展开。直到出现目标状态为止。
具体过程:
1 每次取出队列首元素(初始状态),进行拓展
2 然后把拓展所得到的可行状态都放到队列里面
3 将初始状态删除
4 一直进行以上三步直到队列为空。
广度优先搜索框架
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
B深度优先搜索(DFS)
基本思想:从初始状态,利用规则生成搜索树下一层任一个结点,检查是否出现目标状态,若未出现,以此状态利用规则生成再下一层任一个结点,再检查,重复过程一直到叶节点(即不能再生成新状态节点),当它仍不是目标状态时,回溯到上一层结果,取另一可能扩展搜索的分支。采用相同办法一直进行下去,直到找到目标状态为止。
具体实现过程
1 每次取出栈顶元素,对其进行拓展。
2 若栈顶元素无法继续拓展,则将其从栈中弹出。继续1过程。
3 不断重复直到获得目标状态(取得可行解)
或栈为空(无解)。
深度优先搜索框架
递归实现:
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
扩展
二分查找算法
简单定义:在一个单调有序的集合中查找元素,每次将集合分为左右两部分,判断解在哪个部分中并调整集合上下界,重复直到找到目标元素。
时间复杂度:O (logn),优于直接顺序查找O(n)
代码
int low=0,high=n,mid,res = -1; //low:集合下界 high:集合上节
while(low<=high)
{
mid=(low+high)/2; //mid:将集合分割为两部分
if(num[mid]==x) //查找到符合元素x
{
res = mid;
break;
}
else if(num[mid]<x) //x在右边部分,调整集合下界
low=mid+1;
else //x在左边部分,调整集合上界
high=mid-1;
}
三分法
类似二分的定义Left和Right
mid = (Left + Right) / 2
midmid = (mid + Right) / 2;
如果mid靠近极值点,则Right = midmid;
否则(即midmid靠近极值点),则Left = mid;
核心代码
double mid, midmid;
while ( low + eps < high )
{
mid = (low + high) / 2;
midmid = (mid + high ) / 2;
double cmid = cal(mid);
double cmidmid = cal(midmid);
if ( cmid > cmidmid )
high = midmid;
else
low = mid;
}
double cal(double x)
{
return (h * D - H * x) / (D - x) + x;
//这里放要求的函数;
}
动态规划
什么是动态规划?(一)动态规划是解决多阶段决策问题的一种方法。多阶段决策问题:如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策。多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果.最优性原理:不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。(二)动态规划实际上就是一种排除重复计算的算法,更具体的说,动态规划就是用空间换取时间。
最优决策序列的子序列,一定是局部最优决策子序列。
包含有非局部最优的决策子序列,一定不是最优决策序列。
动态规划的指导思想
在做每一步决策时,列出各种可能的局部解
依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。
以每一步都是最优的来保证全局是最优的。
动态规划问题的一般解题步骤
1、判断问题是否具有最优子结构性质,若不具备则不能用动态规划。
2、把问题分成若干个子问题(分阶段)。
3、建立状态转移方程(递推公式)。
4、找出边界条件。
5、将已知边界值带入方程。
6、递推求解。
图模块
并查集
什么是并查集?英文:Disjoint Set,即“不相交集合”
将编号分别为1…N的N个对象划分为不相交集合,
在每个集合中,选择其中某个元素代表所在集合。
常见两种操作:
合并两个集合
查找某元素属于哪个集合
其带路径压缩的查找算法
find(x)
{
r = x;
while (set[r] <> r) //循环结束,则找到根节点
r = set[r];
i = x;
while (i <> r) //本循环修改查找路径中所有节点
{
j = set[i];
set[i] = r;
i = j;
}
}
最小生成树问题
生成树:由G的n-1条边构成的无环的子图,这些边的集合成为生成树。
最小生成树:所有生成树中权值最小的一个边集T为最小生成树,确定树T的问题成为最小生成树问题。
prim算法
kruskal算法
prim算法的基本思想
任取一个顶点加入生成树;
在那些一个端点在生成树里,另一个端点不在生成树里的边中,取权最小的边,将它和另一个端点加进生成树。
重复上一步骤,直到所有的顶点都进入了生成树为止。
Prim算法:
(1) 任意选定一点s,设集合S={s}
(2) 从不在集合S的点中选出一个点j使得其与S内的某点i的距离最短,则(i,j)就是生成树上的一条边,同时将j点加入S
(3) 转到(2)继续进行,直至所有点都己加入S集合。
算法
nt prim(int s)//s为初始加入的点
{
int i,j,sum=0;
for(i=1;i<=n;i++)
closest[i]=10000000;
for(i=1;i<=n;i++)
closest[i]=map[s][i];
closest[s]=0;
int now;
for(i=1;i<n;i++)
{
int min=INT_MAX;
for(j=1;j<=n;j++)
if(closest[j]&&closest[j]<min)
{
min=closest[j];
now=j;
}
sum+=min;
closest[now]=0;
for(j=1;j<=n;j++)
if(map[now][j]&&map[now][j]<closest[j])
closest[j]=map[now][j];
}
return sum;
}
邻接表(前向星)存储的优先队列优化prim
#define LL long long
const int N = 2000;
const int INF = 1 << 30;
struct Node
{
int v,next,w;
bool operator < (const Node &a) const
{
return w > a.w;
}
} p[N],t1,t2;
int dis[N],vis[N],head[N],cnt;
int res;
void addedge(int u,int v,int w)
{
p[cnt].v = v;
p[cnt].next = head[u];
p[cnt].w = w;
head[u] = cnt++;
}
void prim()
{
priority_queue<Node> q;
for(int i = head[0] ; i != -1 ; i = p[i].next)
{
int v = p[i].v;
if(p[i].w < dis[v])
{
dis[v] = p[i].w;
t1.w = dis[v];
t1.v = v;
q.push(t1);
}
}
vis[0] = 1;
while(!q.empty())
{
t1 = q.top();
q.pop();
int u = t1.v;
if(vis[u]) continue;
vis[u] = 1;
res += dis[u];
for(int i = head[u]; i != -1; i = p[i].next)
{
int v = p[i].v;
if(!vis[v] && dis[v] > p[i].w)
{
dis[v] = p[i].w;
t2.v = v;
t2.w = dis[v];
q.push(t2);
}}}
}
kruskal算法的基本思想
对所有边从小到大排序;
依次试探将边和它的端点加入生成树,如果加入此边后不产生圈,则将边和它的端点加入生成树;否则,将它删去;
直到生成树中有了n-1条边,即告终止。
算法的时间复杂度O(eloge)
数据结构
一维数组,将所有边按从小到大的顺序存在数组里面
并查集
先把每一个对象看作是一个单元素集合,然后按一定顺序将相关联的元素所在的集合合并。能够完成这种功能的集合就是并查集。
§对于并查集来说,每个集合用一棵树表示。
§它支持以下操作:
Union (Root1, Root2) //合并两个集合;
Findset(x) //搜索操作(搜索编号为x所在树的根)。
§树的每一个结点有一个指向其父结点的指针。
代码:
#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;
}
优先队列优化Kruscal算法
略(太多)
最短路问题
在网图中,最短路径是指两顶点之间经历的边上权值之和最短的路径。
问题描述:给定带权有向图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中;
#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中
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算法
还有些小的东西没有总结里面去,我做的题不算太多,还有个别题都还没搞太懂,哎哎哎。不得不说大一的就是厉害!题做的快还多。
我的收获不仅是知识方面,还有那坚定 、不怕吃苦、不怕困难的精神。现在还有一直的一种收获的喜悦。嘻嘻 不悔 致敬!