ACM最终总结

ACM,Association for Computing Machinery , 即国际计算机学会,ICPC,International Collegiate Programming Contest , 即国际大学生程序设计竞赛。
选修课ACM程序设计,是为了ACM开设的课程,或者说是为了选拔有意向参加ACM程序设计大赛的人。这门课就现在已经结束的课时而言,是教授关于ACM设计的部分算法知识,贪心算法、深度搜索和广度搜索算法、动态规划(DP)算法、最短树图算法,这四个算法就是这个学期开设的ACM程序设计课程所教导的。
当然就算法知识来说,这些还差很多,毕竟现在这门课只是一个入门,一个对ACM,对算法的入门。
选择这门课,对个人的实际的编写冗长的代码的能力没有什么大的提升,但是根据算法写程序的能力有了很大的提升,对一些复杂的问题的分析解决能力有了大的进步。然后就是在程序中BUG的出现次数略有减少,虽然还是很多,但是因为做题的原因,改正BUG的能力反而大大增强。再就是学会了一些算法思路,还有学会了许多有用的函数、STL,即标准数据库,这个标准数据库个人感觉对自己在编程时有很大的提升,还让自己对于字符串、数组等一些数据类型的处理更加得心应手。除去这些,ACM的训练还让我个人对于代码的简化、调试的能力有了很大的加强,也就是对于BUG的改正能力强了许多。
对于学到的算法,我做了一个总结。
最先学习的是贪心算法。
贪心算法是指求出当前状态的最优解,贪心算法准确度说不是一个算法,而是一种思维方式,一种对解题的思想。
我个人的理解(讲真,贪心算法的意思刚学的第一个周里根本没绕明白,花了一个星期去寻思题意,才明白的)是一个大的问题分开来求,将一个大打问题划分为小问题,然后再求出这些小问题的最优解,然后小问题最优解合起来就是一整个大问题的最优解。
整个贪心算法没有固定的思路,只是选取一个合适的贪心策略(讲道理,这个贪心策略到底指啥,我从开始学一直到现在都不理解它究竟是个什么东西,但是这并不妨碍做题,凭借做题出来的感觉,完全可以解决),利用贪心策略去求解一个题。
贪心算法牵扯到的题型有三种:背包问题、区间问题以及哈夫曼树问题。
背包问题很经典的一个就是往背包里放置东西,只需要将符合当前最大利益的选取出来就好。
区间问题我记忆深刻的就是搬桌子问题,需要选取当前不冲突的对象,掌握好并行和串行即可。
哈夫曼树只是听他们讨论过,这次的训练中有一道是那个题,那是我剩下的几道题中的一道,我对哈夫曼树的理解就是一个倒置的类型独特的搜索,仅此而已。毕竟没深入做过一道哈夫曼树的问题,只能说这个比较差。
贪心算法中我取了很典型的一个搬运桌子的问题,下面是例子:
概述:一共有400个房间,需要在各个房间内搬运桌子,当搬运桌子的房间之间占用着同一个走廊时,那么每次只能有一张桌子能够被搬运,并且需要等待十分钟,当搬运时不在同一块走廊,那么可以同时搬运这些不重复的桌子。利用编程求给出的数据中能够最快搬运桌子需要花费的时间。
思路:首先将房间号转换为对应的走廊号,然后将搬运桌子时重复的走廊号的次数相加,最后将次数对比求出最大的值,这个最大的值就是需要花费的最大时间。
下面是这道题的解题代码。
#include
#include
using namespace std;
int main()
{
int n, From, To, N;
cin >> n;
while (n--)
{
int move[200] = { 0 };
cin >> N;
for (int i = 0; i < N;++i)
{
cin >> From >> To;
From = (From + 1) / 2;
To = (To + 1) / 2;
if (From > To)
{
int temp = From;
From = To;
To = temp;
}
for (int i = From; i <= To; ++i)
move[i]++;
}
int max = 0;
for (int i = 0; i < 200; ++i)
{
if (move[i] > max)
max = move[i];
}
cout << max * 10 << endl;
}
return 0;
}
然后学习的算法是搜索算法。
 搜索算法分为BFS和DFS。
   如果将一个题目分为多个阶段,那么BFS是在每个阶段里将每个阶段可能出现的情况全数列出,然后再进入下一个阶段。
  DFS是将某个分支所有的情况列出,然后一项项列。搜索算法理解容易,但是做题相对难。
  再就是二分算法,二分算法主要分为二分查找和三分搜索。
   二分查找主要针对的是单调函数给定函数值,求自变量值的情况,非常简单,优点是比较次数少,查找速度快,平均性能好,二分查找的基本思想是将n个元素分成大致相等的两部分,取a[n/2]与x做比较,如果x=a[n/2],则找到x,算法中止;如果xa[n/2],则只要在数组a的右半部搜索x.。这里需要注意的是,不一定非单调函数就不能用二分,有时结合求导。可以求出函数单调性,从而求极值。例如1002题,需要先对给定函数求导,然后再二分,从而求解。
三分查找主要针对的是凸性函数给定函数值,求自变量值的情况,难度较大,是在二分的基础上,对某一区间再次二分的一种算法。
最简单的例子就是已知左右端点L、R,要求找到白点的位置。
思路:通过不断缩小 [L,R] 的范围,无限逼近白点。
做法:先取 [L,R] 的中点 mid,再取 [mid,R] 的中点 mmid,通过比较 f(mid) 与 f(mmid) 的大小来缩小范围。当最后 L=R-1 时,再比较下这两个点的值,我们就找到了解。
整个搜索的题目,主要在于遍历整个数组去找问题的答案,所以相对贪心算法和后来学习的动态规划问题以及最短树问题,这种问题的最麻烦的地方在于整个问题非常繁琐,但是相对的算法的学习和理解不是很难了。
谈到繁琐,这类问题里我所做到的,最繁琐的一个问题是一个关于可乐的问题,我摘出来当作例子。
概述:三个杯子倒可乐,杯子之间互倒,求是否能够均等分开。
思路:杯子可乐为s,m,n,s中为s,mn为0。6种情况:s倒m,s倒n,m倒n,m倒s,n倒m,n倒s。最后n、s中能等分即可。
#include  
#include  
#include  
#include  
#include  
#include  
#include  
using namespace std;  
#define maxn 101  
bool visited[maxn][maxn];  
int m, n, s, si, sj;  
struct node  
{  
    int x, y, all, t;  //x,y,all分别表示m,n,s杯中可乐的体积,t表示倒了多少次  
};  
void BFS()  
{  
    queue que;  
    memset(visited, false, sizeof(visited));  
    node p, q;  
    p.x = 0, p.y = 0, p.t = 0, p.all = s;  
    que.push(p);  
    visited[p.x][p.y] = true;  
    while (!que.empty())  
    {  
        p = que.front();  
        que.pop();  
        if (p.y == p.all && p.y == s / 2)  
        {  
            printf("%d\n", p.t);  
            return;  
        }  
        if (p.all + p.x>m)    //s倒m  
        {  
            q.x = m, q.y = p.y, q.all = p.all + p.x - m, q.t = p.t + 1;  
            if (!visited[q.x][q.y])  
                que.push(q), visited[q.x][q.y] = true;  
        }  
        else  
        {  
            q.x = p.all + p.x, q.y = p.y, q.all = 0, q.t = p.t + 1;  
            if (!visited[q.x][q.y])  
                que.push(q), visited[q.x][q.y] = true;  
        }  
        if (p.all + p.y>n)          //s倒n  
        {  
            q.x = p.x, q.y = n, q.all = p.all + p.y - n, q.t = p.t + 1;  
            if (!visited[q.x][q.y])  
                que.push(q), visited[q.x][q.y] = true;  
        }  
        else  
        {  
            q.x = p.x, q.y = p.all + p.y, q.all = 0, q.t = p.t + 1;  
            if (!visited[q.x][q.y])  
                que.push(q), visited[q.x][q.y] = true;  
        }  
        if (p.x + p.y>n)    //m倒n  
        {  
            q.x = p.x + p.y - n, q.y = n, q.all = p.all, q.t = p.t + 1;  
            if (!visited[q.x][q.y])  
                que.push(q), visited[q.x][q.y] = true;  
        }  
        else  
        {  
            q.x = 0, q.y = p.x + p.y, q.all = p.all, q.t = p.t + 1;  
            if (!visited[q.x][q.y])  
                que.push(q), visited[q.x][q.y] = true;  
        }  
        q.all = p.all + p.x, q.x = 0, q.y = p.y, q.t = p.t + 1; //m倒s  
        if (!visited[q.x][q.y])  
            que.push(q), visited[q.x][q.y] = true;  
        if (p.x + p.y > m)  
        {  
            q.y = p.y + p.x - m, q.x = m, q.all = p.all, q.t = p.t + 1;//n倒m  
            if (!visited[q.x][q.y])  
                que.push(q), visited[q.x][q.y] = true;  
        }  
        else  
        {  
            q.x = p.x + p.y, q.y = 0, q.all = p.all, q.t = p.t + 1;  
            if (!visited[q.x][q.y])  
                que.push(q), visited[q.x][q.y] = true;  
        }  
        q.all = p.all + p.y, q.x = p.x, q.y = 0, q.t = p.t + 1; //n倒s  
        if (!visited[q.x][q.y])  
            que.push(q), visited[q.x][q.y] = true;  
    }  
    printf("NO\n");  
}  
int main()  
{  
    //ifstream cin("in.txt");  
    while (scanf("%d%d%d", &s, &m, &n) && (s || m || n))  
    {  
        if (s % 2)  
        {  
            printf("NO\n");  
            continue;  
        }  
        if (m > n) swap(m, n);  
        BFS();  
    }  
    return 0;  
}  
在学习完搜索的算法以后,第三个学习的算法是动态规划。
相对于贪心算法不是一种解题算法,搜索算法的繁琐,动态规划的问题却有了一定的优点。
动态规划,是求解决策过程最优化的数学方法,是在处理问题过程中,把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解的一种方法,它没有一种确切的解题步骤,它的解决思路是多种多样的。
在算法对应的那套题里边遇到的dp问题主要有斐波那契数列、最长上升子序列问题、还有就是两种背包的问题。
斐波那契数列:f(n)=f(n-1)+f(n-2),这个就是著名的斐波那契数列,然后这类问题是整套题做的最爽的一块,因为主要的步骤在公式,一道题会了,整个就会了,非常简单。
最长上升子序列:就是在一段序列里求出上升是最长一段子序列,解题方式就是用一个数组储存当前选出的最长序列,然后进行比较,如果在有更长的序列,那么替换,否则继续比较,最终输出。这个问题是在前几道是这类的题目,我印象深刻的是一个猴子的。。卡了我好久,一直没搞懂题意,最后搞懂了,也被搞的不想做了。。。
01背包:公式:f[i, j] = max( f[i-1, j-Wi] + Pi (j >= Wi), f[i-1, j] ),给定价值和体积,求在当前情况下可能得到的最大价值。这种类型所有的题目基本都离不开那个公式,很多解题方法最后也都归结到公式。“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c的背包中”,此时能获得的最大价值就是f[v-c]再加上通过放入第i件物品获得的价值w。这就是01背包问题的解题思路。
完全背包:一个复杂的01背包问题,我是这么理解的。。这个类型的题我只做了俩,一个还是转换为了01背包去做。。它跟01背包的差别就是在与取几件的问题,其余的解题差不多。
背包问题,相较于在贪心算法,问题有部分同属于一种问题,也就是一个背包的问题可以同时用这两种算法来解决。但是就像我说的,贪心算法不像是一种算法,所以尽管每一种的动态规划问题解题思路都不尽相同,但是针对某一种问题,它的解题思路以及步骤是相同的,也就是对背包问题而言,它有了一种确切的解题思路,而不是像贪心问题,每一道题都需要去寻找当前状态的最优,尽管相对的动态规划也是单个复杂的状态划分为多个简单的状态。
在动态规划问题中,我选取了一个斐波那契数列的问题和一个背包问题作为例子。
斐波那契数列:概述:六角形的蜂房,小蜜蜂从某个蜂房走向另一个蜂房,每次只能从蜂房的右侧行走,问一共有多少种方式。
思路:每个蜂房内向右走有两个方向,右上、右下,对于不相邻的两个距离最近的蜂房,从中间相隔一排通过时,又是一个斐波那契数列,也就是f(n)=f(n-1)+f(n-2),而对于一个规则的蜂巢来说,从标号的1走到6跟从3走到8是没有差别的,可能的方式是中间间隔的蜂房数。
#include  
#include  
  
using namespace std;  
  
int main()  
{  
    //ifstream cin("in.txt");  
    int n;  
    int a, b, x;  
    cin >> n;  
    long long f[55] = { 0,1,2 };  
    for (int i = 3;i < 51;i++)  
        f[i] = f[i - 1] + f[i - 2];  
    while (n--)  
    {  
        cin >> a >> b;  
        x = b - a;  
        cout << f[x] << endl;  
    }  
    return 0;  
}
背包问题:概述:给一个储蓄罐,开始时候储蓄罐有自己的重量,并且能装的金币有最大限制,然后问在打破罐子可以拿钱买到东西时,最少需要存多少钱。
思路:dp的完全背包问题,将01背包的求解方式倒序一遍就可以,求最小值,最后输出时减去开始时候的罐子重量。
#include  
#include  
#include  
#include  
  
using namespace std;  
  
#define w 10000  
int dp[100005];  
int weight[w], val[w];  
const int INF = 0x7FFFFFF;  
  
  
int min(int a, int b)  
{  
    return a<=b ? a : b;  
}  
  
int main()  
{  
    //ifstream cin("in.txt");  
    int T;  
    cin >> T;  
    while (T--)  
    {  
        memset(weight, 0, sizeof(weight));  
        memset(val, 0, sizeof(val));  
        for (int i = 1;i<100005;i++)  
            dp[i] = INF;  
        dp[0] = 0;  
        int E, F, num;  
        cin >> E >> F;  
        cin >> num;  
        for (int i = 0;i         {  
            cin >> weight[i] >> val[i];  
        }  
        F -= E;  
        for (int i = 0;i         {  
            for (int j = val[i];j <= F;j++)  
            {  
                dp[j] = min(dp[j], dp[j - val[i]] + weight[i]);  
            }  
  
        }  
        if (dp[F] != INF)  
            cout << "The minimum amount of money in the piggy-bank is " << dp[F] << "." << endl;  
        else  cout << "This is impossible." << endl;  
    }  
    return 0;  
}  
第四个算法,也就是最后一个算法,是关于图算法的问题。也就是最小生成树问题和最短路问题。
最小生成树问题包含两个算法,也就是prim算法和kruskal算法,最短路问题两种问题,第一种问题是单源最短路径,包括三个算法,也就是ballman-ford算法、spfa算法和dijkstra算法,另外一种问题是每对顶点的最短路径,它只包括一种算法,是floyd-washall算法。
在最小生成树的问题中,prim算法依赖于强连通图的问题,而kruskal算法在加权连通图中就可以使用。
Prim算法基本算法思想如下:一个加权连通图,其中顶点集合为V,边集合为E,初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空,重复下列操作,直到Vnew = V,在集合E中选取权值最小的边,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一),将v加入集合Vnew中,将边加入集合Enew中,输出:使用集合Vnew和Enew来描述所得到的最小生成树。
Kruskal算法基本思想与prim算法不同,kruskal算法总共选择n- 1条边,(共n个点)所使用的贪婪准则是:从剩下的边中选择一条不会产生环路的具有最小耗费的边加入已选择的边的集合中。注意到所选取的边若产生环路则不可能形成一棵生成树。kruskal算法分e 步,其中e 是网络中边的数目。按耗费递增的顺序来考虑这e 条边,每次考虑一条边。当考虑某条边时,若将其加入到已选边的集合中会出现环路,则将其抛弃,否则,将它选入。
对于最小生成树问题,很经典的一类问题就是城镇连通问题,我选了一道作为例子。
概述:现在给出城镇数目,各个城镇之间的距离,以及已经修建道路的城镇。城镇之间的连通符合最小生成树。求最短需要修建多长的道路可以使城镇连通。
思路:用prim算法,取一个不再以连通的城镇,去权值,选择最小即可。最终输出为所求值。
#include
#include
#include
#include


using namespace std;


const int INF = 1 << 20;
const int maxn = 105;
int N, Q, sum;
int dis[maxn];
int map[maxn][maxn];
bool sign[maxn];


void prim()
{
sign[1] = 1;
for (int i = 2;i <= N;i++)
dis[i] = map[1][i];
int now, min;
for (int i = 1;i < N;i++)
{
min = INF;
for (int j = 1;j <= N;j++)
if (!sign[j] && dis[j] < min)
{
min = dis[j];
now = j;
}
sum += min;
sign[now] = 1;
for (int j = 1;j <= N;j++)
if (!sign[j] && map[now][j] < dis[j])
dis[j] = map[now][j];
}
}


int main()
{
//ifstream cin("in.txt");
while (cin >> N)
{
sum = 0;
memset(sign, 0, sizeof(sign));
for (int i = 1;i <= N;i++)
for (int j = 1;j <= N;j++)
cin >> map[i][j];
cin >> Q;
int a, b;
for (int i = 1;i <= Q;i++)
{
cin >> a >> b;
map[a][b] = map[b][a] = 0;
}
prim();
cout << sum << endl;
}
return 0;
}
然后就是最短路问题,包括ballman-ford算法、spfa算法和dijkstra算法,以及floyd-washall算法。
其中,bellman-ford,spfa,dijkstra算法都使用了松弛技术,对每个顶点v∈V,都设置一个属性d[v],用来描述从源点s到v的最短路径上权值的上界,而floyd-washall算法运用了类似动态规划的递推。
在这个算法里,我截取了一道题目当作例题。
概述:输入数据有多组,每组的第一行是三个整数T,S和D,表示有T条路,和草儿家相邻的城市的有S个,草儿想去的地方有D个;接着有T行,每行有三个整数a,b,time,表示a,b城市之间的车程是time小时;(1=<(a,b)<=1000;a,b 之间可能有多条路),接着的第T+1行有S个数,表示和草儿家相连的城市; 接着的第T+2行有D个数,表示草儿想去地方。
思路:最短路问题,用一次dijkstra算法就可以解决。将草儿的家以及邻镇看作0,然后求想去的地方的最短路。
#include
#include
#include
#include
#include
#include


const int MAX = 1005;
int map[MAX][MAX];
int sign[MAX];
int s[MAX], f[MAX];
int v[MAX];
int d[MAX];
const int INF = 1 << 20;
int T, S, D,n;


int max(int a, int b)
{
if (a < b)
return b;
else return a;
}


void dijkstra()
{

int i, j;
for (i = 1;i <= n;i++)
d[i] = map[1][i];
v[1] = 1;
for (i = 1;i {
int now, min = INT_MAX;
for (j = 1;j <= n;j++)          
{
if (!v[j] && min>d[j])
{
min = d[j];
now = j;
}
}
v[now] = 1;
for (j = 1;j <= n;j++)         
if (!v[j] && d[now] + map[now][j] d[j] = d[now] + map[now][j];
}
}




using namespace std;


int main()
{
//ifstream cin("in.txt");
int a, b, time;
while (scanf("%d%d%d",&T,&S,&D))
{
n = 0;
for (int i = 1;i < MAX;i++)
{
for (int j = 1;j < MAX;j++)
map[i][j] = INF;
map[i][i] = 0;
}
while (T--)
{
scanf("%d%d%d", &a, &b, &time);
n = max(max(n, a), b);
if (time < map[a][b])
map[a][b] = map[b][a] = time;
}
int minn = INF;
for (int i = 1;i <= S;i++)
{
scanf("%d", &s[i]);
map[1][s[i]] = map[s[i]][1] = 0;
}
for (int i = 1;i <= D;i++)
scanf("%d", &f[i]);
dijkstra();
for (int i = 1;i <= D;i++)
minn = min(minn, d[f[i]]);
printf("%d\n", minn);
}
return 0;
}
这个学期,ACM课程总共就学习了这四种算法,接下来的新学期里,因为工作的原因,不能参加ACM集训,所以可能在大学像这样正式的接触ACM是不太可能了,但是我会自己去学习算法,因为算法提了我整个人的能力。如同这堂ACM。

你可能感兴趣的:(ACM终结)