ACM学习总结
Posted lancefate
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ACM学习总结相关的知识,希望对你有一定的参考价值。
ACM学习总结
接触ACM也有一阵子了,刚开始感觉挺有意思的,寒假里老师发了一个刷题的网址,边学边刷了一阵子。 第一次刷题的时候,一看全都是英文的,内心是拒绝的,后来发现虽然看起来都市英文,但是好像都认识呀,也没什么大不了的。
开学刚开始上ACM的时候,还是充满新鲜感的,下了课会第一时间去刷题,反反复复的调代码,有时候调的崩溃了,嘴上说着再也不调了,结果第二天还是第一时间打开电脑继续调,好吧,有种乐此不疲的感觉。但是学了一个学期后,现在只有一个感觉,就是累,心好累啊。做一道题好费事啊,虽然做完成就感很大,但是随着时间的推移,成就感越来越少了。不过在这一学期的学习中,学到了很多东西,增加了我对算法的理解,而且,通过学习ACM,也让我能更简单的理解数据结构。
1. STL
这个东西,就讲了一节课,主要还是在假期里老师让自学的,算是一些很方便的工具吧。
(一) 栈 (Stack)
头文件:#include <stack>
定义:stack<data_type>stack_name;
如:stack<int> s;
操作:
empty() -- 返回bool型,表示栈内是否为空(s.empty() )
size() -- 返回栈内元素个数(s.size() )
top() -- 返回栈顶元素值(s.top() )
pop() -- 移除栈顶元素(s.pop(); )
push(data_type a) -- 向栈压入一个元素a(s.push(a); )
(二) 队列(queue)
头文件:#include <queue>
定义:queue<data_type> queue_name;
如:queue<int> q;
操作:
empty() -- 返回bool型,表示queue是否为空 (q.empty())
size() -- 返回queue内元素个数 (q.size())
front() -- 返回queue内的下一个元素 (q.front())
back() -- 返回queue内的最后一个元素(q.back())
pop() -- 移除queue中的一个元素(q.pop();)
push(data_type a) -- 将一个元素a置入queue中(q.push(a);)
(三) 动态数组(Vector)
头文件: #include <vector>
定义:vector <data_type>vector_name;
如:vector <int> v;
操作:
empty() -- 返回bool型,表示vector是否为空 (v.empty() )
size() -- 返回vector内元素个数 (v.size() )
push_back(data_type a) 将元素a插入最尾端
pop_back() 将最尾端元素删除
v[i] 类似数组取第i个位置的元素(v[0] )
(四) sort
头文件: #include<algorithm>
sort(begin, end);
sort(begin, end, cmp);
例:
int num[] = {1,5,6,2,9};
1) sort(num, num + 5);//默认从小到大排序num[] = {1,2,5,6,9};
2) bool cmp(int a, int b){
return a > b;
}
sort(num, num + 5, cmp); //num[] = {9,6,5,2,1};
(五) set 和 multiset
头文件: #include <set>
定义:set <data_type> set_name;
如:set <int> s;//默认由小到大排序
如果想按照自己的方式排序,可以重载小于号。
struct new_type{
int x, y;
bool operator < (const new_type &a)const{
if(x != a.x) return x < a.x;
return y < a.y;
}
}
set <new_type> s;
操作:
s.insert(elem) -- 安插一个elem副本,返回新元素位置。
s.erase(elem) -- 移除与elem元素相等的所有元素,返回被移除 的元素个数。
s.erase(pos) -- 移除迭代器pos所指位置上的元素,无返回值。
s.clear() -- 移除全部元素,将整个容器清空。
迭代器举例:
multiset <int> :: iterator pos;
for(pos = s.begin(); pos != s.end(); pos++)
... ...
(六) map和multimap
头文件:#include <map>
定义:map <data_type1,data_type2> map_name;
如:map<string, int> m;//默认按string由小到大排序
操作:
m.size() 返回容器大小
m.empty() 返回容器是否为空
m.count(key) 返回键值等于key的元素的个数
m.lower_bound(key) 返回键值等于key的元素的第一个可安插的位置
m.upper_bound(key) 返回键值等于key的元素的最后一个可安插的位置
m.begin() 返回一个双向迭代器,指向第一个元素。
m.end() 返回一个双向迭代器,指向最后一个元素的下一个 位置。
m.clear() 讲整个容器清空。
m.erase(elem) 移除键值为elem的所有元素,返回个数,对 于map来说非0即1。
m.erase(pos) 移除迭代器pos所指位置上的元素。
直接元素存取:
m[key] = value;
查找的时候如果没有键值为key的元素,则安插一个键值为key的新元素,实值为默认(一般0)。
m.insert(elem) 插入一个元素elem
a)运用value_type插入
map<string,float> m;
m.insert(map<string,float>:: value_type ("Robin", 22.3));
b) 运用pair<>
m.insert(pair<string,float>("Robin", 22.3));
c) 运用make_pair()
m.insert(make_pair("Robin",22.3));
(七) 优先队列(priority_queue)
头文件: #include <queue>
定义:priority_queue<data_type> priority_queue_name;
如:priority_queue <int> q;//默认是大顶堆
操作:
q.push(elem) 将元素elem置入优先队列
q.top() 返回优先队列的下一个元素
q.pop() 移除一个元素
q.size() 返回队列中元素的个数
q.empty() 返回优先队列是否为空
2. 贪心算法
算是第一次正式接触的ACM算法,刚开始理解起来挺难得,后来仔细看了课件,顿时有种“原来如此”的感叹。这时候刚刚开始接触,每次AC一道题,心里还是很激动的。
在求最优解问题的过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解,这种求解方法就是贪心算法。
贪心法的基本思路:
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到某算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1. 不能保证求得的最后解是最佳的;
2. 不能用来求最大或最小解问题;
3. 只能求满足某些约束条件的可行解的范围。
实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;
例题分析
1、[背包问题]有一个背包,背包容量是M=150。有7个物品,物品可以分割成任意大小。
要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 | A | B | C | D | E | F | G |
重量wi | 35 | 30 | 60 | 50 | 40 | 10 | 25 |
价值 pi | 10 | 40 | 30 | 50 | 35 | 40 | 30 |
分析:
目标函数: ∑pi最大
约束条件是装入的物品总重量不超过背包容量:∑wi<=M(M=150)
(1)根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
(2)每次挑选所占重量最小的物品装入是否能得到最优解?
(3)每次选取单位重量价值最大的物品,成为解本题的策略。 ?
值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。
贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
可惜的是,它需要证明后才能真正运用到题目的算法中。
一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。
对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:
(1)贪心策略:选取价值最大者。反例:
W=30
物品:A B C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。
(2)贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。
(3)贪心策略:选取单位重量价值最大的物品。反例:
W=30
物品:A B C
重量:28 20 10
价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。
所以需要说明的是,贪心算法可以与随机化算法一起使用,具体的例子就不再多举了。(因为这一类算法普及性不高,而且技术含量是非常高的,需要通过一些反例确定随机的对象是什么,随机程度如何,但也是不能保证完全正确,只能是极大的几率正确)
3. 搜索
搜索这个东西在数据结构里也讲过了,当初有事请了假,这里没怎么学好,幸好在学数据结构的时候把这里补上来了,搜索这个东西,感觉还是很有意思的。
二分搜索法
二分搜索法,是通过不断缩小解可能存在的范围,从而求得问题最优解的方法。在程序设计竞赛特别是ACM中,经常可以见到二分搜索法和其他算法结合的题目。
广度优先搜索(BFS)
基本思想:从初始状态S 开始,利用规则,生成所有可能的状态。构成的下一层节点,检查是否出现目标状态G,若未出现,就对该层所有状态节点,分别顺序利用规则。
生成再下一层的所有状态节点,对这一层的所有状态节点检查是否出现G,若未出现,继续按上面思想生成再下一层的所有状态节点,这样一层一层往下展开。直到出现目标状态为止。
具体过程:
1 每次取出队列首元素(初始状态),进行拓展
2 然后把拓展所得到的可行状态都放到队列里面
3 将初始状态删除
4 一直进行以上三步直到队列为空。
深度优先搜索(DFS)
基本思想:从初始状态,利用规则生成搜索树下一层任一个结点,检查是否出现目标状态,若未出现,以此状态利用规则生成再下一层任一个结点,再检查,重复过程一直到叶节点(即不能再生成新状态节点),当它仍不是目标状态时,回溯到上一层结果,取另一可能扩展搜索的分支。采用相同办法一直进行下去,直到找到目标状态为止。
具体实现过程
1 每次取出栈顶元素,对其进行拓展。
2 若栈顶元素无法继续拓展,则将其从栈中弹出。继续1过程。
3 不断重复直到获得目标状态(取得可行解)或栈为空(无解)。
4. 动态规划
多阶段决策问题:如果一类问题的求解过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,并影响到下一个阶段的决策。
多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果.
最优性原理
不论初始状态和第一步决策是什么,余下的决策相对于前一次决策所产生的新状态,构成一个最优决策序列。
最优决策序列的子序列,一定是局部最优决策子序列。
包含有非局部最优的决策子序列,一定不是最优决策序列。
动态规划的指导思想
在做每一步决策时,列出各种可能的局部解
依据某种判定条件,舍弃那些肯定不能得到最优解的局部解。
以每一步都是最优的来保证全局是最优的。
动态规划的基本模型
动态规划问题具有以下基本特征:
问题具有多阶段决策的特征。
每一阶段都有相应的“状态”与之对应,描述状态的量称为“状态变量”。
每一阶段都面临一个决策,选择不同的决策将会导致下一阶段不同的状态。
每一阶段的最优解问题可以递归地归结为下一阶段各个可能状态的最优解问题,各子问题与原问题具有完全相同的结构。
动态规划问题的一般解题步骤
1、判断问题是否具有最优子结构性质,若不具备则不能用动态规划。
2、把问题分成若干个子问题(分阶段)。
3、建立状态转移方程(递推公式)。
4、找出边界条件。
5、将已知边界值带入方程。
6、递推求解。
经典例子
0-1背包问题
1.问题描述:一共n中物品,每样物品只有一件,物品i的重量为wi>0,价值为vi,背包的最终容量为weight,求如何添加物品,在不超过背包容量的情况下,背包中物品的价值最大?
分析: 这是一道典型的动态规划题目,当然可以用回溯法枚举每一种可能,最后求出最大值,但是回溯法的搜索空间为2^n,即每一种物品都有选和不选之分,选择该物 品为1,为右子树,不选该物品为0,在为左子树,这样的一个搜索空间为2^n,复杂度为O(2^n). 由于0-1背包问题很显然含有很多子问题,画出回溯法时的解空间树就可以知道有很多重叠的子树,因此我们考虑用动态规划方法求解.
2.动态规划方法,找出最优子结构.
设Z={z1, z2, z3, ....,zn}是一个最优解,其中zi=1表示背包中含有该物品,zi=0表示背包中不含该物品.0-1背包问题Knap(n, weight)在此时的价值为dp[n][weiht].下面为0-1背包的最优子结构.
(1) 如果zn=1,那么dp[k-1][weight- w[n]] + v[n] > dp[n-1][weight],而且{z1,z2,…,zn-1}是Knap(n-1,weight- w[n])的最优解
(2) 如果zk=0,那么dp[n-1][weight- w[n]) + v[n] <= dp[n-1][weight],而且{z1,z2,…,zn-1}是Knap(n-1,weight)的最优解。
证明:
(1)如果dp[k-1][weight - w[n]] + v[n] <=dp[n-1][weight],则有dp[n][weight]= dp[n-1][weight - w[n]]+ v[n] <= dp[n-1][weight],从而得出Z={z1,z2,…,zn}不是最优解,与前提矛盾。因此dp[n-1][weight - w[n]] + v[n] > dp[n-1][weight]。假设{z1,z2,…,zn-1}不是Knap(n-1,weight- w[n])的最优解,则存在一个解使得f[n-1][weight] > dp[n-1][weight],则f[n-1][weight]+v[n] > dp[n-1][weight - w[n]] + v[n] = dp[n][weight],与假设矛盾,所以{z1,z2,…,zn-1}是Knap(n-1,MaxWeight- w[n])的最优解。
(2) 证法与1)类似。
3. 0-1背包问题的递推式
dp[j] = max{dp[j], dp[j-w[i]]+v[i]}, 其中1=<i<=n, w[i]<=j<=weight. dp[j]表示背包中重量为j时的最大价值
回文串问题
1.问题描述:给出一个字符串,包含大写字母,小写字母和数字,例如Ab3bd,只有插入操作,一次只能添加一个 字符,最终的目的是使其成为回文串,即为对称的字符串,上例中的结果可以是Adb3bdA,此时添加2个字符,分别是第二个字符d和最后一个字符A,使其 成为回文串.
2.动态规划找出最优子结构:
si表示给出的字符串的第i个字符,此问题的最优子结构可以描述为:若(si,......, sj )为插入字符最少获得的回文串,则(s(i+1),...., s(j-1))也必须为插入最少字符而获得的回文串.
证明:假设 (s(i+1), ...., s(j-1))不是插入最少字符而获得的回文串,那么一定存在另一种插入方式(s'(i+1), ...... , s'(j-1))获得到回文串的插入次数少于 (s(i+1), ...., s(j-1)),而此时与 (si, ......, sj)为插入字符最少获得的回文串相矛盾,因此该问题具有最优子结构.
3.自顶向下的递推公式
dp[i][j]表示第i个字符到第j个字符时的子字符串(子问题)获得回文串需要的插入次数,
dp[i][j] = dp[i+1][j-1], 当s[i]=s[j]时
dp[i][j] = min(dp[i][j-1], dp[i+1][j] )+1, 当s[i]!=s[j]时
5. 图算法
图的定义:
很简单,G(V,E), V、E分别表示点和边的集合。
图的表示:
主要有两种,邻接矩阵和邻接表,前者空间复杂度,O(V2),后者为O(V+E)。因此,除非非常稠密的图(边非常多),一般后者优越于前者。
图的遍历:
宽度遍历BFS(start): (1)队列Q=Empty,数组bool visited[V]={false...}. Q.push(start);
(2)while (!Q.empty()){
u = Q.pop(); visited[u] = true; //遍历u结点
foreach(u的每一个邻接结点v) Q.push(v);
}
深度遍历DFS(start): (1)栈S=Empty, 数组bool visited[V]={false...}. S.push(start);
(2)while (!S.empty()){
u= S.pop();
if(!visited[u]) visited[u] = true; //遍历u结点
foreach(u的每一个邻接结点v) S.push(v);
}
初看之下两个算法很 相似,主要区别在于一个使用队列,一个使用栈,最终导致了遍历的顺序截然不同。队列是先入先出,所以访问u以后接下来就访问u中未访问过的邻接结点;而栈的后进先出,当访问u后,压入了u的邻接结点,在后面的循环中,首先访问u的第一个临接点v,接下来又将v的邻接点w压入S,这样接下来要访问的自然是w 了。
最小生成树:
一.Prime算法:
(1) 集合MST=T=Empty,选取G中一结点u,T.add(u)
(2) 循环|V|-1次:选取一条这样的边e=min{(x,y)| x in T, y in V/T} T.add(y);MST.add(e);
(3)MST即为所求
二. Kruskal算法
(1)将G中所有的边排序并放入集合H中,初始化集合MST=Empty,初始化不相交集合T={{v1}, {v2}...}},也即T中每个点为一个集合。
(2) 依次取H中的最短边e(u,v),如果Find-Set(u)!=Find-Set(v)(也即u、v是否已经在一棵树中),那么Union(u,v)(即u,v合并为一个集合),MST.add(e);
(3)MST即为所求
这两个算法都是贪心算法,区别在于每次选取边的策略。证明该算法的关键在于一点:如果MST是图G的最小生成树,那么在子图G'中包含的子生成树MST' 也必然是G'的最小生成树。这个很容易反正,假设不成立,那么G'有一棵权重和更小的生成树,用它替换掉MST',那么对于G我们就找到了比MST更小的生成树,显然这与我们的假设(MST是最小生成树)矛盾了。
理解了这个关键点,算法的正确性就好理解多了。对于Prime,T于V/T两个点集都会各自有一棵生成树,最后要连起来构成一棵大的生成树,那么显然要选两者之间的最短的那条边了。对于Kruskal算法,如果当前选取的边没有引起环路,那么正确性是显然的(对给定点集依次选最小的边构成一棵树当然是最小生成树了),如果导致了环路,那么说明两个点都在该点集里,由于已经构成了树(否则也不可能导致环路)并且一直都是挑尽可能小的,所以肯定是最小生成树。
最短路径:
这里的算法基本是基于动态规划和贪心算法的,经典算法有很多个,主要区别在于:有的是通用的,有的是针对某一类图的,例如,无负环的图,或者无负权边的图等。
单源最短路径
(1) 通用(Bellman-Ford算法):
(2) 无负权边的图(Dijkstra算法):
(3) 无环有向图(DAG) 所有结点间最短路径:
(1)Floyd-Warshall算法:
(2) Johnson算法:
6. 总结
特别喜欢那句“生死看淡,不服就干”,真的很热血。转眼一个学期就过去了,感觉这个学期真的是很忙,不过也真的是学到了点东西。算法这个东西,感觉真的是太重要了。但是我们学到的还只是皮毛。虽然想继续研究下去,但是没有那么多的时间。刚开始的时候,一天有一个AC,一整天都是开心哒,后来,学的东西多了,感觉好难哦,现在想想,都不知道这么长时间是怎么熬过来的。没办法,程序员就是这么苦这么累,既然选择了这个行业,也就只能风雨兼程了。
以上是关于ACM学习总结的主要内容,如果未能解决你的问题,请参考以下文章