清华大学ACM集训队培训资料(内部使用)
一、C++基础
基本知识
所有的C++程序都是有函数组成的,函数又叫做子程序,且每个C++程序必须包含一个main函数,编译器(能够把源代码转换成目标代码的程序)把翻译后的目标代码和一些启动代码组合起来,生成可执行文件,main函数就是可执行文件的入口,所以,每个C++程序有且只有一个main函数。
下面我们看一个最简单C++程序。(程序1.1)
程序1.1
intmain(){return 0;}
在这个程序中,如果缺少任何一个字符,编译器就无法将其翻译成机器代码。
此外,C++是对大小写敏感的,这就意味着,如果我将mian()函数拼为Main(),哪么,编译器在编译这段程序的时候就会出错。
编辑源文件
能够提共管理程序开发的所有步骤,包括编辑的程序成为集成开发环境(integrateddevelopment evironments, IDE)。在windows系统下,使用较为广泛的有Microsoft Visual C++、Dev-Cpp等,在UNIX系统下,有Vim、emacs、eclipes等。这些程序都能提供一个较好的开发平台,使我们能够方便的开发一个程序,接下我们所要了解的都是标准C++,所有源代码都在Dev-cpp下编写,能够编译通过。
如果我们修改程序1.1中的main()函数的名称,将其改为Main(),那么,IDE就会给出错误信息,比如“ [Linker error] undefined reference to`WinMain@16'”,因为编译器没有找到main函数。
接下来,我们来看一个经典的C++例子(程序1.2)
程序1.2
#include
using namespacestd;
int main(void)
{
cout<<"HelloWrold!"< return 0; } 运行结果 Hello World! 程序说明 第一行“#include 另外一个C++程序例子 // ourfunc.cpp -- defining your ownfunction #include void simon(int); // function prototype for simon() int main() { using namespace std; simon(3); // call thesimon() function cout << "Pick an integer: "; int count; cin >> count; simon(count); // call it again cout << "Done!" << endl; return 0; } void simon(int n) // define the simon() function { using namespace std; cout << "Simon says touch your toes " << n<< " times." << endl; } // void functions don't need return statements 下面试运行情况: Simon says touch your toes 3 times. Pick an integer: 512 Simon says touch your toes 512 times. Done! 程序中包含了cin语句来从键盘上获取数据。 该程序中包含了除main函数以外的另一个函数simon(),他和main函数定义的格式相同,函数的统一格式如下: type functionname (argumentlist) { statements } 注意,定义simon()的代码在main()函数的后面,C++中不允许将函数定义在另一个函数内。每个函数的定义都是独立的,所有的函数的创建都是平等的。 simon()函数的函数头定义如下: void simon(intn) 以void 开头表明simon()没有返回值,因此我们不能类是这样的使用它。 simple =simon(3); 有返回值的函数如下 // convert.cpp -- converts stone to pounds #include int stonetolb(int); //function prototype int main() { using namespace std; int stone; cout << "Enter the weight in stone: "; cin >> stone; int pounds = stonetolb(stone); cout << stone << " stone = "; cout << pounds << " pounds." << endl; return 0; } int stonetolb(int sts) { return 14 * sts; } 下面是运行情况: Enter the weight in sone: 14 14 stone = 196 pounds. 程序通过cin语句给stone提供一个值,然后在main函数中,把这个值传递给stonetolb()函数,这个植被赋给sts之后,stonetolb()用return 将 14*sts返回给main()。 函数头中的int表明stonetolb()将返回一个整数。 除了int类型之外,C++的内置数据类型还有:unsigned long、long、unsigned int、unsigned short、short、char、unsigned char、signed char、bool、 float、double、long double。 对于数据的输入和输出有几道练习题 http://acm.hdu.edu.cn/showproblem.php?pid=1089 至 http://acm.hdu.edu.cn/showproblem.php?pid=1096 二、算法基础 1. 什么是算法 算法是完成特定任务的有限指令集。所有的算法必须满足下面的标准: a. 输入。由外部题共零个或多个输入量。 b. 输出。至少产生一个输出量。 c. 明确性。每条指令必须清楚,不具模糊性。 d. 有限性。如果跟踪算法的指令,那么对于所有的情况,算法经过有限步以后终止。 e. 有效性。每条指令必须非常基础,原则上使用笔和纸就可以实现 例 选择排序 void SelectionSort(Type a[], int n) //Sort the arrat a[1:n] into nondecreasingorder. { for(int i=1; i<=n; i++) { intj=1; for(int k=i+1; k<=n; k++) if(a[k] < a[j]) j= k; Typet = a[i]; a[i]= a[j]; a[j]= t; } } 使用该函数时,应将Type替换为C++中的数据类型 3.性能分析 程序P所用时间定义为T(P), T(P)是编译时间和运行时间之和。 下面我们计算一下选择排序运行时所要花费的时间 SelectionSort cost times for (int i=1; i<=n; i++) c1 { int j=1; c2 for (int k=i+1; k<=n; k++) c3 if (a[k] < a[j]) c4 j = k; c5 Type t = a[i]; a[i] = a[j]; a[j] = t; c6 } 那么该算法运行的时间 那么,在最坏的条件下,的值应该是 所以,算法的运行时间为 4.渐进符号 定义: [大O]函数,念做是的大”oh”,当且仅当存在正常数和,使得对于所有的,有。 例 对于所有有,所以。 对于所有有,所以 对于所有有,所以 当然对于所有有,所以 定义: [Ω]函数,念做是的”omega”,当且仅当存在正常数和,使得对于所有的,有。 例 对于所有有,所以。 当然,但是。 现然无论是O还是Ω,都不能精确的描述一个函数 定义: [Θ]函数,念做是的”theta”,当且仅当存在正常数和,使得对于所有的,有。 例 对于有且,所以 Θ记号要比O和Ω都要精确。 排列生成器Θ(n!) void Perm(Type a[], int k, int n) { if(k==n){ //Output permutation. for(int i-1; i } else//a[k:n] has more than one permutation. // Generate these recursively. for(int i=k; i<=n; i++){ Typet=a[k]; a[k]=a[i]; a[i]=t; Perm(a,k+1, n); //Allpermutations of a[k+1:n] t=a[k];a[k]=a[i]; a[i]=t; } } 对于下面的程序
#include
using namespace std;
void Perm(int a[], int k, int n)
{
if (k { int i, t; for (i=k;i { t = a[k]; a[k] = a[i]; a[i] = t; Perm(a, k+1, n); t = a[k]; a[k] = a[i]; a[i] = t; } } else { int i; for (i=0;i { cout<
} cout< } } int main(void) { int a[3] = {1,2, 3}; Perm(a, 0, 3); return 0; }
该程序的运行结果为
1 2 3
1 3 2
2 1 3
2 3 1
3 2 1
3 1 2
那么,该函数就完成了对一个数组进行全排列的操作
下面,分析该程序,我用圆圈代表每次函数的调用
每次函数的调用都用序号表示
1. a: 1 2 3 k: 0
2. a: 1 2 3 k: 1
3. a: 1 2 3 k: 2
4. a: 1 3 2 k: 2
5. a: 2 1 3 k: 1
6. a: 2 1 3 k: 2
7. a: 2 3 1 k: 2
8. a: 3 2 1 k: 1
9. a: 3 2 1 k: 2
10. a: 3 1 2 k: 2
排列生成器的另外一个版本
他将输出给定n个布尔变量的所有可能的组合
void Perm (bool a[], int k, int n)
{
if(k == n)
{
//statement
}
else
{
a[k]= true;
Perm(a,k+1, n);
a[k]= false;
Perm(a,k+1, n);
}
}
在上次冬季赛上有这么一道题
竞赛真理
JUNNY在经历了无数次学科竞赛的失败以后,得到了一个真理:做一题就要对一题!但是要完全正确地做对一题是要花很多时间(包括调试时间),而竞赛的时间有限。所以开始做题之前最好先认真审题,估计一下每一题如果要完全正确地做出来所需要的时间,然后选择一些有把握的题目先做。当然,如果做完了预先选择的题目之后还有时间,但是这些时间又不足以完全解决一道题目,应该把其他的题目用贪心之类的算法随便做做,争取“骗”一点分数。
根据每一题解题时间的估计值,确定一种做题方案(即哪些题目认真做,哪些题目“骗”分,哪些不做),使能在限定的时间内获得最高的得分。
INPUT FORMAT:
从标准输入(cin,scanf等)读入数据。数据有多组,先输入K(K组数据)。每组第一行有两个正整数N和T,表示题目的总数以及竞赛的时限(单位秒)。以下的N行,每行4个正整数W1i 、T1i 、W2i 、T2i ,分别表示第i题:完全正确做出来的得分,完全正确做出来所花费的时间(单位:秒),“骗”来的分数,“骗”分所花费的时间(单位秒)。其中,3 ≤ N ≤ 30,2 ≤ T ≤ 1080000,1 ≤ W1i 、W2i ≤ 30000,1 ≤ T1i 、T2i ≤ T。
OUTPUT FORMAT:
直接把所求得的最高得分输出。数据之间需换行。
SAMPLE INPUT:
2
4 10800
18 3600 3 1800
22 4000 12 3000
28 6000 0 3000
32 8000 24 6000
3 7200
50 5400 10 900
50 7200 10 900
50 5400 10 900
SAMPLE OUTPUT :
50
70
下面我们对问题进行简化。我们只要考虑是做题还是不做题。
从标准输入(cin,scanf等)读入数据。数据只有一组,先输入K(K组数据)。每组第一行有两个正整数N和T,表示题目的总数以及竞赛的时限(单位秒)。以下的N行,每行2个正整数Wi 、Ti,分别表示第i题:做出来的得分和做出来所花费的时间(单位:秒),OUTPUT FORMAT:
直接把所求得的最高得分输出。数据之间需换行。
SAMPLE INPUT:
5 10
1 20
5 10
4 15
3 20
2 10
SAMPLE OUTPUT :
65
下面是用全排列生成器完成的代码
#include
using namespace std;
int m;
int t[20][2];
int tSum;
void work(bool a[], int n);
void f(bool a[], int k, int n)
{
if(k < n)
{
a[k]= true;
f(a,k+1, n);
a[k]= false;
f(a,k+1, n);
}
else
{
work(a,n);
}
}
void work(bool a[], int n)
{
intx;
inttime=0, score=0;
for(x=0; x { if(a[x]) { score+= t[x][1]; time+= t[x][0]; } } if(time <= tSum) { if(score > m) { m= score; } } } int main(void) { boola[30]; intn, c; cin>>n>>tSum; m= 0; for(c=0; c { cin>>t[c][0]; cin>>t[c][1]; } f(a,0, n); cout< return0; }
通过一个排列生成器将所有的可能送入work()函数内,然后work函数找到在时间范围内的最高的分数。
现在我们将其优化,将work()和f()合并,就能得到更好的程序
#include
using namespace std;
int m;
int t[20][2];
int tSum;
void dfs(int k, int n, int cScore, intcTime)
{
if(k < n)
{
dfs(k+1,n, cScore , cTime);
dfs(k+1,n, cScore + t[k][1], cTime + t[k][0]);
}
else
{
if(cTime <= tSum)
{
if(cScore > m)
{
m= cScore;
}
}
}
}
int main(void)
{
intn, c;
cin>>n>>tSum;
m= 0;
for(c=0; c { cin>>t[c][0]; cin>>t[c][1]; } dfs(0,n, 0, 0); cout< return0; }
这个程序就是深度优先搜索,如果n非常大,递归调用的次数是非常惊人的,达到次。为了减少递归的次数,我们可以采取剪枝的手段,在递归下一次前判断是否可行。如果肯定不能就停止递归,节省时间。
#include
using namespace std;
int m;
int t[20][2];
int tSum;
void dfs(int k, int n, int cScore, intcTime)
{
if(k < n)
{
dfs(k+1,n, cScore , cTime);
if(cTime < tSum)
{
dfs(k+1,n, cScore + t[k][1], cTime + t[k][0]);
}
}
else
{
if(cTime <= tSum)
{
if(cScore > m)
{
m= cScore;
}
}
}
}
int main(void)
{
intn, c;
cin>>n>>tSum;
m= 0;
for(c=0; c { cin>>t[c][0]; cin>>t[c][1]; } dfs(0,n, 0, 0); cout< return0; }
为了达到更好的剪枝效果,可以在搜索前对数据进行排序。竞赛真理原题也可用这种思想去解。更复杂的算法将在以后进行讲解。
引自:不详