每次刷题都觉得自己吃了知识点不全,基础不牢固的亏,刷题的时候目标也不明确,于是看完了算法笔记并把知识点归纳了一下,当然直接看书会更加详细,这个归纳只是学习时加深印象以及方便自己之后回顾而已;之后刷题大概会根据这个大纲归纳一下具体题型,半个月之后看自己能刷多少吧^ _ ^
类型 | 取值范围 | 占用字节 | 格式符 |
---|---|---|---|
int | 10的9次方以内整数 | 4字节(32位) | %d |
long long | 10的10次方~10的18次方 | 8字节(64位) | %lld |
float | 6~7位有效精度 | 32字节 | %f |
double | 15~16位有效精度 | 64字节 | %lf |
char | -128~127 | -128~127 | %c(字符串%s不加&) |
bool | 0/1 | 0/1 |
long long赋值后要加LL;
整型加unsigned表示无符号,会把法术范围挪到正数上来;
用double,放弃float;
ASCII码——09(4857)、AZ(6590)、az(97122) 小写字母比大写大32;
输出字符串格式scanf("%s",str);
%md
不足m位的int变量右对齐输出,高位空格补齐;如果本身超过m位,则保持原样
%0md
不足m位的int变量右对齐输出,高位加0补齐;如果本身超过m位,则保持原样
%.mf
浮点数保留m位小数输出
f a b s ( d o u b l e x ) — — 取 绝 对 值 fabs(double x)——取绝对值 fabs(doublex)——取绝对值
p o w ( d o u b l e r , d o u b l e p ) — — r 的 p 次 方 pow(double r,double p)——r的p次方 pow(doubler,doublep)——r的p次方
f l o o r ( d o u b l e x ) / c e i l ( d o u b l e x ) — — 向 / 下 上 取 整 floor(double x) / ceil(double x)——向/下上取整 floor(doublex)/ceil(doublex)——向/下上取整
p o w ( d o u b l e r , d o u b l e p ) — — 返 回 r p pow(double r,double p)——返回r^p pow(doubler,doublep)——返回rp
s q r t ( d o u b l e x ) — — 算 术 平 方 根 sqrt(double x)——算术平方根 sqrt(doublex)——算术平方根
l o g ( d o u b l e x ) — — 取 e 的 对 数 ( l o g a b = l o g e b / l o g e a ) log(double x)——取e的对数(log_ab=log_eb/log_ea) log(doublex)——取e的对数(logab=logeb/logea)
s i n ( d o u b l e x ) / c o s ( d o u b l e x ) / t a n ( d o u b l e x ) / a s i n ( d o u b l e x ) / a c o s ( d o u b l e x ) / a t a n ( d o u b l e x ) 正 余 弦 sin(double x)/cos(double x) / tan(double x) /asin(double x) / acos(double x) /atan(double x) 正余弦 sin(doublex)/cos(doublex)/tan(doublex)/asin(doublex)/acos(doublex)/atan(doublex)正余弦
r o u n d ( d o u b l e x ) — — 四 舍 五 入 round(double x)——四舍五入 round(doublex)——四舍五入
struct 类型名{
//除自己外的所有数据类型
}结构体变量名;
“.”(普通变量)or"->’’(指针
结构体内部可以自定义初始化函数,示例
struct studenInfo{
int id;
char gender;
studenInfo(int _id,char _gender){
id=_id;
gender=_gender;
}
//可以简化为一行
//studenInfo(int _id,char _gender):id(_id),gender(_gender){}
}stu,*p;
对每个元素赋相同的值(0/-1)
格式:memset(a,-1,sizeof(a));
const double eps=1e-8;
#define Equ(a,b) (fabs((a)-(b))<(eps))
时间复杂度
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n 2 ) O(1)
p s : 两 个 f o r 循 环 的 n 不 能 超 过 1000 , 因 为 空 间 复 杂 度 运 算 次 数 n 2 不 能 超 过 1 0 7 ps:两个for循环的n不能超过1000,因为空间复杂度运算次数n^2不能超过10^7 ps:两个for循环的n不能超过1000,因为空间复杂度运算次数n2不能超过107
ps:两个for循环的n不能超过1000,因为空间复杂度不能超过
空间复杂度
看数组大小或者其他数据结构大小
编码复杂度
算法冗长,复杂度就大
题目怎么说就·怎么做,不涉及算法
给定元素,然后查找某个满足条件的元素。一般简单查找就是两个for遍历,查找算法第四章涉及
找规律输出
定义二维数组输出
平年闰年/大月小月问题,需要细心
//判断闰年函数
bool isLeap(int year){
return (year%4==0&&year%100!=0)||(year%400==0)//整除400或者整除4但不整除100
}
//公式法
int y=0,pro=1;//y为最后十进制数,pro为每次的倍数
while(x!=0){
y=y+(x%10)*pro;//获得每次x的个位数
x=x/10;//去掉x的个位数
pro=pro*P;//倍数增长
}
//除基取余法
int z[40],num=0;//z存放Q进制y的每一位,num为位数
do{
z[num++]=y%Q;
y=y/Q;
}while(y!=0)//商不为0时才循环,z数组从高位到低位为q进制数
仔细分析输入输出格式,会有一些细节和边界情况,积累经验,熟练相关函数
gets | ges(str) | puts | puts(str) |
---|---|---|---|
strcmp(str1,str2) | 比较字符串大小(1<2(-)) | strcpy(ste1,str2) | 复制2—>1 |
strcat(str1,str2) | str2拼接到str1后面 | strlen(str) | 字符个数 |
sscanf(str,"%d",&n);
sprintf(str,"%d",n);
//可以复杂一点,格式和scanf/printf差不多
//i从[0,n-1]枚举,待排序部分[i,n-1],从小到大,选出最小
int selectSort(){
for(int i=0,i
//相邻元素比较,从小到大,有序输出
int bubbleSort(){
for (int i=0; i a[j + 1]){
int temp=a[j];//交换位置
a[j]=a[j+1];
a[j+1]=temp;
}//左边数更大就换
}
}
return 0;
}
//将数组无序部分插入已有序部分
int insertSort(){
for(int i=1;i=0) && (key
#include
编写bool cmp()函数
sort(首地址,尾地址的下一个,cmp)
将元素通过一个函数转换为整数,时该整数可以尽量唯一的代表这个元素
const int maxn=10010;
bool hashTable[maxn]={false};
key是整数:
1.直接定址法 2.平方取中法 3. 除留余数法
冲突三种方法:
1.线性探查法 2.平方探查法 3.链地址法
举例:将二维坐标P映射成整数H§=x*Range+y;
字符串hash是指将字符串S映射成整数 AZ=025,az=2651,整数52~62或者直接拼接
int hashFunc(char S[],int len){
int id=0;
for(int i=0;i='Z'){
id=id*52+(S[i]-'A');
}else if(S[i]<='a'&&S[i]>='z'){
id=id*52+(S[i]-'a')+26;
}
}
return 0;
}
分解--------解决---------合并
递归边界:分解的尽头
递归式:将原问题分解成子问题的手段
相关问题:---------全排列------n皇后问题
--------考虑当前状态下局部最优
总的来说,贪心是用来解决一类最优化问题,并希望由局部最优解来推得全局最优解的算法思想.贪心算法适用的问题一定满足最优子结构性质,即一个问题的最优解可以由子问题的最优解有效构造出来.
#include
//a[]严格递增,一般采用非递归
int binarySearch(int a[],int left,int right,int x){
int mid;
while(left<=right){
mid=(left+right)/2;
if(a[mid]==x) return mid;
else if(a[mid]>x){
right=mid-1;
}else{
left=mid+1;
}
}
return -1;//查找失败
}
int main(){
const int n=10;
int a[n]={1,2,3,4,5,6,7,8,9,10};
printf("%d",binarySearch(a,0,n-1,6));
return 0;
}
ps:如果二分上界超过int数据一半,可能溢出,此时用mid=left+(right-left)/2代替mid=(left+right)/2
求近似平方根问题/装水问题/木棒切割问题
快速幂又叫二分幂,基于以下事实:
1. 如 果 b 是 偶 数 , 那 么 a b = a ∗ a b − 1 1.如果b是偶数,那么a^b=a*a^{b-1} 1.如果b是偶数,那么ab=a∗ab−1
2. 如 果 b 是 奇 数 , 那 么 a b = a b / 2 ∗ a b / 2 2.如果b是奇数,那么a^b=a^{b/2}*a^{b/2} 2.如果b是奇数,那么ab=ab/2∗ab/2
typedef long long LL;
//求a^b%m
LL binaryPow(LL a,LL b,LL m){
if(b==0)
return 1;
else if(b%2==1) //可以用if(b&1)代替
return a*binaryPow(a,b-1,m)%m;
else{
int mul=binaryPow(a,b/2,m);
return mul*mul%m;
}
}
typedef long long LL;
//求a^b%m
LL binaryPow(LL a,LL b,LL m){
LL ans=1;
while(b>0){
if(b&1){
ans=ans*a%m;
}
a=a*a%m;
b>>=1;
}
return ans;
}
利用问题本身和序列特性,使用两个下标i,j对序列进行扫描(同向或反向),从而以较低复杂度(一般是O(n))解决问题
//a[n]递增序列,正整数M,求a[i]+a[j]=M,two points思想示例
while(i
2-路归并排序思想:------将序列不断两两分组,组内单独排序,然后不断合并.
const int maxn=100;
//将数组的[L1,R1]与[L2,R2]区间合井为有序区间(此处L2即为R1 +1)
void merge(int A[],int L1,int R1,int L2,int R2){
int i=L1,j=L2;//i指向A[L1],j指向A[L2]
int temp[maxn],index=0;//temp临时存放合并后的数组,index为下标
while(i <= R1&&j<=R2){
if(A[i] <= A[j]) {
temp[index++] = A[i++];//将A[i]加入序列temp
}else{
temp[index++] = A[j++];//将A[j]加入序列temp
}
}
while(i <= R1) temp[index++] = A[i++]; //将[L1, R1]的剩余元素加人
while(j <= R2) temp[index++] = A[j++]; //将[L2, R2]的剩余元素加入
for(i = 0; i < index; i++) {
A[L1+i]=temp[i];//将合并后的序列赋值回数组A
}
}
void mergeSort(int A[],int left,int right){
if(left
把A[1]存入temp,让A[1]左边数都比他小,右边数都比他大的问题:
//递归实现
int Partition(int A[],int left,int right){
int temp=A[left];
while(lefttemp) right--;
A[left]=A[right];
while(left
#include//程序必备
#include//随机数必备
#include//随机数必备
int main(){
srand((unsigned)time(null));//main方法第一句,生成随机数种子
for(int i=0;i<10;i++){
printf("%d",rand());
}
return 0;
}
注意:
生成的是[0,RAND_MAX]范围内整数,如果想输出[a,b]范围内,需要使用rand()%(b-a+1)+a;显然rand()%(b-a+1)的范围是[0,b-a],再加上a就是[a,b]
想生成更大范围的随机数
(int)(round(1.0rand()/RAND_MAX(b-a)+a)
一种思想,细心考虑找出题目中递推关系.
原理:类似于随即快速排序算法,
问题:从一个无序数组中求得第K大的数字
//递归实现
int randPartition(int A[],int left,int right){
int temp=A[left];
while(lefttemp) right--;
A[left]=A[right];
while(left
掌握简单的数理逻辑,就是些水题,ccf第一题那种.
--------------------实现原理:欧几里得算法(辗转相除法)
int gcd(int a,int b){
if(b==0) return a;
else return gcd(b,a%b);
}
//更简洁的写法
int gcd(int a,int b){
return !b?a:gcd(b,a%b);
}
int main(){
int d=gcd(a,b);
printf("%d",(a*b/d));
}
所谓的分数的四则运算是指,给定两个分数的分子和分母,求他们加减乘除的结果
分数的表示-------对个分数来说, 最简洁的写法就是写成假分数的形式, 即无论分子比分母大或者小,都保留其原数。因此可以使用一个结构体来存储这种只有分子和分母的分数:
于是就可以定义Fraction 类型的变量来表示分数,或者定义数组来表示一堆分数。其中需要对这种表示制订三项规则:
①使down为非负数。如果分数为负,那么令分子up为负即可。
②如果该分数恰为0,那么规定其分子为0,分母为1。
③分子和分母没有除了1以外的公约数。
struct Fraction{//分数
int up, down;//分子、分母
}
分数的化简-------分数的化简主要用来使Fraction变量满足分数表示的三项规定,因此化简步骤也分为以下三步:
①如果分母down为负数,那么令分子up和分母down都变为相反数。
②如果分子up为0,那么令分母down为1.
③约分:求出分子绝对值与分母绝对值的最大公约数d,然后令分子分母同时除以d.
代码如下:
Fraction reduction (Fraction result){
if(result.down < 0) {
//分母为负数,令分子和分母都变为相反数
result.up = -result.up;
result.down = - result.down;
}
//如果分子为0,令分母为1
if (result.up == 0) {
result.down = 1;
}
//如果分子不为0,进行约分
else {
int d=gcd(abs (result .up), abs (result dow)); //分子分母的最大公约数
result.up /= d;//约去最大公约数
result.down /= d;
return result;
}
result=(f1.up*f2.down+f1.down*f2.up)/(f1.down*f2.down)
result=(f1.up*f2.down-f1.down*f2.up)/(f1.down*f2.down)
result=(f1.up*f2.up)/(f1.down*f2.down)
result=(f1.up*f2.down)/(f1.down*f2.up)
分数的输出根据题目的要求进行,但是大体上有以下几个注意点:
①输出分数前,先化简
②如果分数r的分母down为1,该分数是整数,一般来说题目会要求直接输出分子而省略分母。
③如果分数r的分子up的绝对值>分母down (想一想分子为什么要取绝对值? ),说明该分数是假分数,此时应按带分数的形式输出,即整数部分为r.up /r.down,分子部分为
abs(r.up) % r.down,分母部分为r.down.
④以上均不满足时说明分数r是真分数,按原样输出即可。
以下是一个输出示例:
void showResult (Fraction r) {
//输出分数,先分数化简
r=reduction(r) ;
//整数
if(r.down==1) printf("%lld", r.up) ;
//假分数
else if(abs(r.up) > r.down) {
printf("%d %d/%d", r.up / r.down, abs(r.up) %r.down, r.down) ;
} else {
//真分数
printf ("%d%d", r.up,r.down) ;
/*强调一点:由于分数的乘法和除法的过程中可能使分子或分母超过int型表示范围,因
此一般情况下,分子和分母应当使用long long型来存储。*/
1不是素数,也不是合数
如果存在被整除的数,一定有一个小于sqrt(n),一个大于sqrt(n)
bool isPrime(int n){
if(n==1) return false;
int sqr=(int)sqrt(n*1.0);
for(int i=2;i<=sqr;i++){
if(n%i==0) return false;
}
return ture;
}
//n为10的5次方以内的范围
const int maxn=101;
int Prime[maxn],pNum=0;
bool p[maxn]={0};
int Find_Prime(){
for(int i=1;i
------------如果要求更大的数,则启用
//n为10的5次方以内的范围
const int maxn=101;
int Prime[maxn],pNum=0;
bool p[maxn]={0};
int Find_Prime(){
for(int i=2;i
------------将一个正整数分解成一个或多个质数的乘积的形式,分解步骤:
创建一个结构体:
struct factor{
int x,cnt;//x_质因子,cnt_其个数
}fac[10];//fac数组开到10就可以了,不论题目
枚举1~sqrt(n)所有质因子p,判断是否是n的质因子
int num=0;
if(n%prime[i]==0){//如果是质因子
fac[num].x=prime[i];//增加质因子
fac[num].cnt=0;//初始化个数
while(n%prime[i]==0){
fac[num].cnt++;//计算个数
n/=prime[i];
}
num++;//检查下一个质因子
}
上述步骤结束后仍然大于1,还有个大于sqrt(n)的质因子n
if(n!=1){//无法被除尽
fac[num].x=n;//把n放进去
fac[num++].cnt=1;
}
-----------定义:高精度的整数,就是无法用基础数据类型存储其精度的整数
struct bign{
int len;
int d[1000];
bign(){
memset(d,0,sizeof(d));
int len=0;
}//每次结构体变量被定义都会自动初始化
}
bign change(char str[]){
bign a;
a.len=strlen(str);
for(int i=0;i
类似于列竖式:
//高精度加法
bign add(bign a,bign b){
bign c;
int carry=0;
for(int i=0;i
//高精度减法
bign sub(bign a,bign b){
bign c;
int carry=0;
for(int i=0;i=1&&c.d[c.len-1]==0){//去除最高位的0且至少保留一位最低位
c.len--;
}
}
return c
}
//注意point:使用sub前比较两个数的大小,如果被减数小于减数,则调换数组相减,结果加上负号
//高精度与低精度乘法
bign multi(bign a,int b){
bign c;
int carry=0;
for(int i=0;i<a.len;i++){
int temp=a.d[i]*b+carry;
c.d[c.len++]==temp%10;
carry=temp/10;
}whiel(carry!=0){
c.d[c.len++]=carry%10;
carry=carry/10;
}
return c
}
高精度与低精度除法
bign multi(bign a,int b,int &r){
//r为余数
bign c;
c.len=a.len;//被除数与商位数一一对应,长度相等
for(int i=a.len-1;i>=0;i--){
r=r*10+a.d[i];
if(r<b) c.d[i]=0//不够除
else{
//够除
c.d[i]=r/b;
r=r%b;
}
}
while(c.len-1>=1&&c.d[c.len-1]==0){
//去除最高位的0且至少保留一位最低位
c.len--;
}
return c
}
暂时不学,有需要再回来
n ! 中 有 ( n / p + n / p 2 + n / p 3 + . . . ) 个 质 因 子 n!中有(n/p+n/p^2+n/p^3+...)个质因子 n!中有(n/p+n/p2+n/p3+...)个质因子
//n!中有多少个质因子p
int cal(int n,int p){
int ans=0;
while(n){
ans+=n/p;
n/=p;
}
return ans;
}
上面这个可以推广计算n!的末尾有多少个0(等于n!中因子10的个数)
一个概念:
n!中质因子p的个数,实际上等于1~n中p的倍数的个数n/p加上n/p!中质因子p的个数
//因此得出cal的递归版本
int cal(int n,int p){
if(n<p) return 0;//n
else return n/p+cal(n/p,p)//返回n/p!中质因子p的个数**
}
方法一:根据定义式计算------暴力破解,容易溢出,忽略
方法二:根据递推式计算
递 归 公 式 : C n m = C n − 1 m + C n − 1 m − 1 递归公式:C_n^m=C_{n-1}^m+C_{n-1}^{m-1} 递归公式:Cnm=Cn−1m+Cn−1m−1
递 归 边 界 : C n 0 = C n n = 1 递归边界:C_n^0=C_n^n=1 递归边界:Cn0=Cnn=1
#define long long LL;
LL res[67][67]={0};
LL C(LL n,LL m){
if(m==0||n==m) return 1;
else if(res[n][m]!=0) return res[n][m];//记录已经计算过的C[n][m],防止重复计算
else return res[n][m]=C(n-1,m)+C(n-1,m-1);//赋值并返回
}
方法三:定义式变形
#define long long LL;
LL C(LL n,LL m){
LL ans=1;
for(LL i=1;i<=m;i++){
ans=ans*(n-m+1)/i;
}
return ans;
}
#define long long LL;
LL res[67][67]={
0};
LL C(LL n,LL m){
if(m==0||n==m) return 1;
else if(res[n][m]!=0) return res[n][m];//记录已经计算过的C[n][m],防止重复计算
else return res[n][m]=C(n-1,m)+C(n-1,m-1)%p;//赋值并返回
}
//使用筛法得到素数表prime,注意表中最大素数不得小于n
int prime[maxn];
//计算C(n,m)%p
int C(int n,int m,int p){
int ans = 1;
//遍历不超过n的所有质数
for(int i = 0; prime[i] <= n;i++) {
//计算C(n,m)中prime[i]的指数c,cal (n,k)为n!中含质因子k的个数
int C = cal(n, prime[i]) - cal (m, prime[i]) -cal(n - m, prime[i]);
//快速幂计算prime[i]^c%p
ans = ans * binaryPow(prime[i], C,p)%p;
return ans;
int lucas(int n,int m){
if(m==0) return 1;
else return C(n%p,m%p)*Lucas(n/p,m/p)%p;
}//原理没看,代码不长可以死记,或者看书189页
示例 | n | m | p | 方法 |
---|---|---|---|---|
case1 | <=10^4 | <=10^4 | <=10^9 | 方法一 |
case2 | <=10^6 | <=10^6 | <=10^9 | 方法二 |
case3 | <=10^18 | <=10^18 | <=10^9(p是素数) | 方法四 |
方法三太长了,以后再看,一般情况下方法一就够用了。
---------------------------------------------***(Standard Template Library)***
-----------------------------------------------#include
中文译名:“向量”,容易理解的叫法:“变长数组”,即“长度根据需要自动改变的数组”
#include
//格式
vector<typename> name;
//基本数据类型举例
vector<int> array[arraySize];
//注意:typename是STL容器是,>>之间必须加空格,举例
vector<vector<int> > vi;
下标访问
范围0~v.size-1,比如v[0],v[1]等
迭代器访问
迭代器iterator类似于指针,通过*it来访问
//格式
vector<typename>::iterator it;
//举例
vector<int>::iterator it=vi.begin();
it++;it--;
//*(it+1)等价于vi[i]
vi.push_back(x);//尾部添加元素
vi.pop_back(x);//删除尾元素
vi.size();//获得元素个数
vi.clear();//清除所有元素
vi.insert(it,x);//向it位置插入x
vi.erase(it);//删除迭代器为it位置的元素
vi.erase(first,last)//删除[first,last)范围内的所有元素
中文译名:“集合”,即“内部自动有序且不含重复元素的容器”,字面理解可知set内元素自动递增,且自动去除重复元素
#include
//格式
set<typename> name;
//基本数据类型举例
set<int> array[arraySize];
//注意:typename是STL容器是,>>之间必须加空格,举例
set<set<int> > st;
只能用迭代器访问,不支持*(it+i)来访问,只能枚举
st.size();//获得元素个数
st.clear();//清除所有元素
st.insert(x);//插入x
st.find(x);//返回对应值为x的迭代器
//删除单个元素-------------------------------------------------
st.erase(st.find(x));//删除迭代器为it位置的元素,可以搭配find(x)
st.erase(x);//直接删除值为x的set元素
//删除多个元素-------------------------------------------------
st.erase(first,last)//删除[first,last)范围内的所有元素,可以搭配find(x)
自动去重以及升序排序。
延申:set元素唯一,不唯一用multiset。另外C++ 11中·增加了unordered_set,只去重不排序。
#include //注意和string.h不一样
string str="str";
通过下标访问和迭代器访问都可以,而且迭代器可支持直接+i;
读入与输出一般cin/cout,如果想用printf,可以str.c_str()将string类型强制转换为字符数组。
operator +;//字符串拼接,str1+str+2
compare operator:包括==,!=,<,<=,>,>=//按字典序比较
str.size() / str.length();//字符串长度
str.insert();//插入
str.insert(pos,string);//pos位置插入string
str.insert(it,it2,it3);//[it2,it3)位置字符串插入it位置
//删除单个元素-------------------------------------------------
str.erase(it);//删除迭代器为it位置的元素
//删除多个元素-------------------------------------------------
str.erase(first,last);//删除[first,last)范围内的所有子串
str.erase(pos,length);//pos插入位,length删除长度
str.clear();//清除所有元素
str.substr(pos,len);//返回pos位置长度为len的字符串
str.find(str);//寻找字符串str,没找到返回string:npos;
str.find(str,pos);//寻找pos开始往后的字符串str,没找到返回string:npos;
string:npos;//常数,-1
str.replace(pos,len,str2);//把pos位置开始长度为len的字符串替换为str2
str.replace(it1,it2,str2);//把[it1,it2)的字符串替换成str2
中文译名:“映射”,即“将任何基本类型(包括STL容器)映射到任何基本类型(包括STL容器)”,map会以键值从小到大排序,键值对唯一。
#include
//格式
map<typename1,typename2> name;//将typename1(键key)映射到typename2(值value)
//基本数据类型举例
map<set<int>,string> mp;
下标:mp[key]
迭代器:it->first访问key,it-second访问value
mp.find(key);//寻找键值为key的迭代器
//删除单个元素-------------------------------------------------
mp.erase(it);//删除迭代器为it位置的元素
mp.erase(key);//删除key对应的元素
//删除多个元素-------------------------------------------------
mp.erase(first,last);//删除[first,last)范围内的所有子串
mp.size();//获得映射对数
mp.clear();//清除所有元素
先进先出,类似排队,队首指针front,队尾指针rear
//先进先出,类似排队,队首指针front,队尾指针rear
#include
queue q; //其中Type为数据类型(如 int,float,char等)
q.push(item) //q[++rear]; 将item压入队列尾部
q.pop() //front++; 删除队首元素,但不返回
q.front() //q[front+1]; 返回队首元素,但不删除
q.back() //q[rear]; 返回队尾元素,但不删除
q.size() //return rear-front; 返回队列中元素的个数
q.empty() //if(front==rear){}; 检查队列是否为空,如果为空返回true,否则返回false
需要实现广度优先搜索时,不手动实现而是用queue做代替
注意:使用front()和pop()之前,必须用empty()判断队列是否为空,否则可能因此出现错误。
优先队列,底层是堆,队首元素是优先级最高的那一个
没有front()/back()函数,只能通过top()来访问队首元素
//先进先出,类似排队,队首指针front,队尾指针rear
#include
priority_queue q; //其中Type为数据类型(如 int,float,char等)
q.push(item) //q[++rear]; 将item压入队列尾部
q.pop() //front++; 删除队首元素,但不返回
q.top() //q[front+1]; 返回队首元素,但不删除
q.size() //return rear-front; 返回队列中元素的个数
q.empty() //if(front==rear){}; 检查队列是否为空,如果为空返回true,否则返回false
解决一些贪心问题(9.8节),或者对Dijkstra算法进行优化
注意:使用top()之前,必须用empty()判断队列是否为空,否则可能因此出现错误。
//格式
priority_queue<typename,vector<typename>,less<typename> > q;
//less表示数字大的优先级大,而greater表示数字小的优先级大
//示例
priority_queue<int,vector<int>,less<int> > q;
priority_queue<double,vector<double>,less<double> > q;
struct fruit{
string name;
int price;
friend bool operater < (fruit f1,fruit f2){
return f1.price>f2.price;
}
};//此时水果价格低的,优先级高
-----------------------------------------------------------
另一种方法看书,暂时准备只记这个
可以推出:
如果基本数据类型或者其他STL容器,也可以通过同样的方式来定义优先级
如果结构体数据较为庞大,建议使用引用提供效率,此时要加上const和&
struct fruit{
string name;
int price;
friend bool operater < (const fruit &f1,const fruit &f2){
return f1.price>f2.price;
}
};//此时水果价格低的,优先级高
栈,后进先出,类似盒子,栈顶指针TOP=-1
s.top();
//后进先出,类似盒子,栈顶指针TOP=-1
#include
stack<Type> s;//其中Type为数据类型(如 int,float,char等)。
s.push(item); //st[++TOP]=item; 将item压入栈顶
s.pop(); //TOP--; 删除栈顶的元素,但不会返回
s.top(); //return st[TOP]; 返回栈顶的元素,但不会删除
s.size(); //return TOP+1; 返回栈中元素的个数
s.empty(); //if(TOP==-1){}; 检查栈是否为空,为空返回true,否则返回false
模拟递归,防止栈内存溢出
“对”,实用的小玩意儿,把两个元素绑定成为一个元素------类似于内部有两个元素的结构体
#include//可以用utility
pair<typename1,typename2> name;
初始化方法:
1. pair<string,int> p("haha",5);
代码中想临时构建的话---------------------------------
1. pair<string,int>("haha",5);
2.make_pair("haha",5);//自带的make_pair函数
p.first(); p.second();
compare operator:
==/!=/<=/>/>=--------------------直接比较first元素,不能比较大小就second
max(int/double x,int/double y);//返回x,y中最大值
min(int/double x,int/double y);//返回x,y中最小值
abs(int x);//返回绝对值
//fabs(double x);返回浮点数绝对值,它在math头文件下
swap(x,y);//交换
reverse(it1,it2);//将数组指针在[it1,it2)的元素进行反转
next_permutation()//给出一个序列在全排列中的下一个序列
//示例
int a[10]={
1,2,3};
do{
printf("%d %d %d\n",a[0],a[1],a[2]);
}while(next_permutation(a,a+3))
//memset到达全排列最后一个时会返回false,必须得用do while
fill()//把某段区间赋值为相同的值
//示例
int a[10]={
1,2,3};
fill(a,a+10,233);//a[0]~a[4]都赋值为233
sort(首地址,尾地址的下一个地址,cmp比较函数);//默认递增排序
cmp();//cmp函数定义根据要比较的规则来写
注意:容器的比较只涉及vector,string,deque可以使用sort,因为set和map本身有序(红黑树实现)
lower_bound(first,last,val);//范围内大于等于val的第一个值的坐标(指针或者迭代器)
upper_bound(first,last,val);//范围内大于val的第一个值的坐标(指针或者迭代器)
//后进先出,类似盒子,栈顶指针TOP=-1
#include
stack s;//其中Type为数据类型(如 int,float,char等)。
s.clear(); //return TOP=-1; 清空栈——注意!STL中没有实现栈的清空,需要自己写
s.push(item); //st[++TOP]=item; 将item压入栈顶
s.pop(); //TOP--; 删除栈顶的元素,但不会返回
s.top(); //return st[TOP]; 返回栈顶的元素,但不会删除
s.size(); //return TOP+1; 返回栈中元素的个数
s.empty(); //if(TOP==-1){}; 检查栈是否为空,为空返回true,否则返回false
//先进先出,类似排队,队首指针front,队尾指针rear
#include
queue q; //其中Type为数据类型(如 int,float,char等)
q.clear(); //front=clear=-1; 清空栈——注意!STL中没有实现栈的清空,需要自己写
q.push(item) //q[++rear]; 将item压入队列尾部
q.pop() //front++; 删除队首元素,但不返回
q.front() //q[front+1]; 返回队首元素,但不删除
q.back() //q[rear]; 返回队尾元素,但不删除
q.size() //return rear-front; 返回队列中元素的个数
q.empty() //if(front==rear){}; 检查队列是否为空,如果为空返回true,否则返回false
不连续的由若干个结点连接成的表,每个结点代表一个元素,由数据域和指针域组成
分为有头结点和无头结点,我们一般创建的时有头结点的
头结点的数据域为空,指向的第一个结点保存第一个data
struct node{
typename data;//数据域
node* next;//指针域
};//定义结点
1.new运算符(推荐)
//格式
typename* p=new typename;
//举例
node* p=new node;
//定义后要释放内存,否则可能造成内存泄漏
delete(p);
2.malloc函数
//格式
typename* p=(typename*)malloc(sizeof(typename));
//举例
node * p=(node*)malloc(sizeof(node));
//定义后要释放内存,否则可能造成内存泄漏
free(p);
#include //标准输入输出
#include //标准库函数
struct node{
int data;
node* next;
};
//创建链表
node* create(int a[]){
node *p,*pre,*head;//p---临时结点,pre----临时结点的前置结点
head=new node;
head->next=null;
pre=head;//把前置结点设置为头结点
for(int i=0;i<5;i++){
p=new node;
p->data=a[i];
p->next=null;
pre->next=p;//前驱节点的指针域指向p
pre=p;
}
return head;//返回创建完成的头结点
}
int main(){
int a[5]={
1,2,3,4,5}
node* L=create(a);
L=L->next;//第一个结点才有数据
while(L!=null){
//指针不指向空地址
printf("%d",L->data);
L=L->next;
}
return 0;
}
从头结点开始遍历查找就行,代码很简单
void insert(node *head,int pos,int x){
//把x插入以head为头结点的链表的第pos位置上
node *p=head;
for(int i=0;i<pos-1;i++){
//只到pos的前一个停下
p=p->next;
}
node *q=new node;//新建插入结点
q->data=x;//数据x
q->next=p->next;//新结点指向插入前原先pos位置的结点
p->next=q;//前一个位置的结点指向新结点
}
void del(node *head,int x){
node *p=head->next;
node *pre=head;
while(p!=null){
if(p->data==x){
pre->next=p->next;
delete(p);
p=pre->next;
}else{
pre=p;
p=p->next;
}
}
}
实现原理是hash,不需要头结点,其他和动态链表差不多
/* 线性表的静态链表存储结构 */
struct Node
{
typename data;//数据域
int cur; //游标,指向下一个数组下标,类似于指针域
}node[size]
//要用到的时候再看细节
全称:“Depth First Search”
概念:是一种枚举所有路径以遍历所有情况的搜索方法。
思路:类似于走迷宫时,一条路一直往前走,走到死胡同折返到前一个岔路口选另一个结点继续往下走,如此往复知道走出迷宫;
实现方法:递归(在递归时系统调用系统栈存放递归每层的状态,所以本质是用栈实现。)
举例题目:
有n件物品,每件物品重量w[i],价值c[i],选若干个物品放入容量为V的背包,求价值最高的组合,0
代码示例
相关问题:和迷宫三个关键点(岔路口—选择,死胡同—掉头边界,出口—最终解决方案)
比如:给定一个序列,枚举这个序列的所有子序列(可以不连续),这个问题也等价于枚举从N个整数中选择K个数的所有方案。
注意:假设每个岔路口(选择点)都可以被选择多次的话,只要根据题目更改代码中选择index号岔路口的部分就行了
全称:“Breadth First Search”
概念:是一种按层次顺序以遍历所有情况的搜索方法。
思路:类似于迷宫时,一条路走到一个岔路口,就把这个岔路口的下一个节点都标记一下,然后再从下一层结点依次这么做,只到找到出口位置。像是石子投入水面,水纹以同心圆方式扩散开来。
实现方法:队列,先进后出
void BFS(int s){
queue<int> q;
q.push(s);
while(!q.empty()){
//取出队首元素top;
//访问队首元素;
// 队首元素出队;
// top下一层结点全部入队;
}
}
给出一个矩阵,矩阵元素0/1,称位置(x,y)与其上下左右四个位置是相邻的,若矩阵有若干个相邻的1,称这些1构成了一个块,求这个矩阵有几个块?
const int maxn=100;
int X[4]={
0,0,1,-1};//增量数组
int Y[4]={
1,-1,0,0};//竖着看,表示上下左右四个方向
int n,m;
int matrix[maxn][maxn];
bool inq[maxn][maxn]={
0};
bool judge(int x,int y){
//判断(x,y)是否需要访问
}
void BFS(int x,int y){
queue<node> q;
Node.x=x,Node.y=y;
q.push(Node);
inq[x][y]=true;
while(!q.empty){
node top=q.front();//取出队首元素
q.pop();
for(int i=0;i<4;i++){
//得到4个相邻位置
int newx=topx+X[i];
int newy=topy+Y[i];
if(judge(newx,newy)){
//如果新位置需要访问
Node.x=newx,Node.y=newy;
q.push(Node);
inq[newx][newy]=1;//新结点入队
}
}
}
}
int main(){
//读入矩阵
//枚举位置
int ans=0;//存放块数
for(0~n-1)
for(0~m-1){
//如果元素为且未入过队
if(matrix[x][y]==1&&inq[x][y]==0){
ans++;//块数加一
BFS(x,y);//访问整个块,该块所有q都标记inq
}
}
//.......
return 0;
}
相关问题:求最少步数,最小路径,最低耗费时间等等
注意:队列实现时,要注意元素入队的push操作时创建了一个元素二点副本入队,因此对这个元素的修改不会影响本元素,如果题目要求改本身元素时,我们可以换个思路:即,把元素所在的数组下标入队,而不是元素本身。
树(tree) | 结点(node) | 叶子结点(leaf) | 根结点(noot) |
---|---|---|---|
边(edge) | 子结点(child) | 子树(subtree) | 空树(empty tree) |
树的层次(layer) | 从根结点为第一层开始算,向下逐层加一,以此类推 |
---|---|
结点的度(degree) | 结点的子树棵数 |
结点深度(depth) | 从根节点(深度为1)自顶向下逐层累加 |
结点高度(height) | 从最底层叶节点(高度为一)自底向上逐层累加 |
森林(forest) | 若干棵树集合 |
孩子节点、父亲结点、兄弟结点
祖先结点(包括父亲结点和自己)、子孙结点(包括孩子结点和自己)
//二叉链表
struct node{
int data; //数据域
node* lchild; //指向左子树
node* rchild; //指向右子树
};
//建树前一般根结点不存在,所以地址设为null;
node* root=null;
二叉树的常用操作有几个,建立/查找/修改/插入/删除——其中删除对不同的二叉树差别比较大,这里就不介绍了;
node* newNode(int v){
node* Node=new node;
Node->data=v;
Node->lchild=Node->rchild=null;
return Node;
}
//递归实现
void search(node* root,int x,int newdata){
if(root==null){
return;//死胡同,空树
}
if(root->data==x){
root->data==newdata;
}
search(root->lchild,x,newdata);//往左子树递归
search(root->rchild,x,newdata);//往右子树递归
}
//insert函数要注意必须对根节点root使用引用,否则无法插入(对root本身修改)
void insert(node* &root,int x){
if(root==null){
;//空树,查找失败,插入
root=newNode(x);
return;
}
if(由于二叉树性质,应该插在left subtree){
insert(root->lchild,x);
}else{
insert(root->rchild,x);
}
}
node* createTree(int data[],int n){
node* root=null;
for(int i=0;i<n;i++){
insert(root,data[i]);
}
return root;
}
可 以 直 接 建 立 一 个 2 k 大 小 的 数 组 存 放 完 全 二 叉 树 , 其 中 k 为 二 叉 树 的 最 大 高 度 可以直接建立一个2^k大小的数组存放完全二叉树,其中k为二叉树的最大高度 可以直接建立一个2k大小的数组存放完全二叉树,其中k为二叉树的最大高度
性质:
完全二叉树任何一个编号为x的结点,它的左孩子编号一定是2x,右孩子编号一定是2x+1;
此外,该数组存放的顺序恰好为完全二叉树的层序遍历序列。
而判断某个结点是否为叶结点的标志为:该结点的左节点2x大于结点总个数n;
判断某个结点是否为空的标志:该结点下标大于结点总数
//根节点——左子树——右子树
void preOrder(node* root){
if(root==null){
return;
}
//最先访问根节点
printf("%d\n",root->data);
preOrder(root->lchild);
preOrder(root->rchild);
}
//左子树——根节点——右子树
void inOrder(node* root){
if(root==null){
return;
}
//最先访问左子树
inOrder(root->lchild);
printf("%d\n",root->data);
inOrder(root->rchild);
}
//左子树——右子树——根节点
void postOrder(node* root){
if(root==null){
return;
}
postOrder(root->lchild);
postOrder(root->rchild);
printf("%d\n",root->data);
}
//层序遍历经常要求计算每个结点所处的层次,因此结构体可以直接添加一个layer变量
//二叉链表
struct node{
int data; //数据域
int layer;
node* lchild; //指向左子树
node* rchild; //指向右子树
};
void LayerOrder(node* root){
queue<*node> q;//注意这里队列是存地址
root->layer=1;
q.push(root);
while(!q.empty){
node* now=q.front();
q.pop();
printf("%d\n",root->data);//访问队首元素
if(now->lchild!=null){
now->lchild->layer=now->layer+1;
q.push(now->lchild);
}
if(now->rchild!=null){
now->rchild->layer=now->layer+1;
q.push(now->rchild);
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FairIzwi-1595856115298)(/…/images/%E7%AE%97%E6%B3%95%E7%AC%94%E8%AE%B0/image-20200630164506472.png)]
//当前先序序列区间为[preL,preR], 中序列区间为[inL,inR],返回根结点地址
node* create(int preL, int preR, int inL, int inR){
if(preL > preR) {
return NULL;//先序序列长度小于等于0时,直接返回
node* root = new node; //新建 一个新的结点,用来存放当前二又树的根结点
root->data - pre[preL]; //新结点的数据域为根结点的值
int k;
for(k = inL; k <= inR; k++) {
if(in[k]==pre[preL]) {
//在中序序列中找到in[k] == pre[L]的结点
break;
}
int numLeft = k-inL; //左子树的结点个数
//左子树的先序区间为[preL+1, preL+numLeft], 中序区间为[inL, k-1]
//返回左子树的根结点地址,赋值给root的左指针
root->lchild = create(preL + 1, preL + numLeft,inL, k - 1) ;
//右子树的先序区间为[preL + numLeft + 1, preR], 中序区间为[k+1, inR]
//返回右子树的根结点地址,赋值给root的右指针
root->rchild = create(preL + numLeft + 1, preR, k + 1, inR);
return root;
//返回根结点地址
}
}
注意:只有中序序列可以和其他序列的任意一个来构建唯一的二叉树,其他无论是两两搭配还是三个一起上都不行,因为中序可以区分出左右子树。
就是把结点的左右指针域改成int型,所有对指针的操作都改为对数组下标的访问,了解一下即可,我还是喜欢指针。
一般意义上的树的结点是散乱而没有顺序的,所以我们不使用动态指针而是一个vector数组存放所有字节点的地址——即,考试遇到一般树使用静态写法
struct node
{
int data;
vector<int> child;//存放所有子节点的下标
}Node[maxn];
//如果题目不涉及数据,这个结点结构体可以简化为:
vector<int> child[maxn];//即图的邻接表示法在树的应用
新建结点
int index=0;
int newNode(int v){
Node[index].data=v;
Node[index].child.clear();//清空子节点
return index++;
}
void preOrder(int root){
printf("%d\n",Node[root].data);
for(int i=0;i<Node[root].child.size;i++){
preOrder(Node[root].child[i]);//递归访问所有子节点
}
}
//层序遍历与二叉树的类似
struct node{
int data; //数据域
int layer;
vector<int> child;
}Node[maxn];
void LayerOrder(int root){
queue<int> q;
q.push(root);
NOde[root].layer=0;//根结点的层号为0(这里看题目吧)
while(!q.empty){
int front=q.front();
q.pop();
printf("%d\n",Node[front].data);//访问队首元素
for(int i=0;i<Node[front].child.size;i++){
int child=Node[front].child[i];
Node[child].layer=Node[front].layer+1;
q.push(child);//将当前所有子节点入队
}
}
}
DFS与先根遍历-------------------- - 二者问题都可以相互转化。
BFS与层序遍历-------------------- - 二者问题都可以相互转化。
“Binary Search Tree”,又叫二叉排序树、二叉搜索树
递归定义如下:
①要么二叉查找树是- -棵空树。
②要么二叉查找树由根结点、左子树、右子树组成,其中左子树和右子树都是二叉查找树,且左子树上所有结点的数据域<=根结点的数据域,右子树上所有结点的数据域>=根结点的数据域。
查找操作
①如果当前根结点root为空,说明查找失败,返回。
②如果需要查找的值x= root->data,说明查找成功,访问之。
③如果需要查找的值x< root->data,说明应该往左子树查找,因此向root->lchild 递归。
④说明需要查找的值x> root->data,则应该往右子树查找,因此向root->rchild递归。
//search函数查找二又查找树中数据域为x的结点
void search (node* root, int x) {
if(root == NULL) {
//空树,查找失败
printf ("search failed\n") ;
return;
}
if(x == root->data){
//查找成功,访问之
printf("&d\n"", root->data);
} else if(x< root->data) {
//如果 x比根结点的数据域小,说明x在左子树
search (root->lchild, x); //往左子树搜索x
} else
(//如果x比根结点的数据域大,说明x在右子树
search (root->rchild,x);//往右子树搜索x
}
//insert函数将在二叉树中插入-一个数据域为x的新结点(注意参数root要加引用&)
void insert(node* &root, int x)
if(root == NULL) [ //空树, 说明查找失败,也即插入位置
root = newNode(x) ;
//新建结点, 权值为x
return;
if(x == root->data) {
//查找成功, 说明结点已存在,直接返回
return;
} else if(x < root->data){
//如果x比根结点的数据域小,说明x需要插在左子树
insert (root->lchild, x); //往左子树搜索 x
} else {
//如果x比根结点的数据域大,说明x需要插在右子树
insert (root->rchild, x); //往右子树搜索x
}
//二叉查找树的建立
node* Create(int data[],int n) {
node* root = NULL; // 新建根结点 root
for (int i =0;1< n; i++) {
insert (root, data[i]);
//将data[0]~data[n-1]插入二叉查找树中
}
return root;
//返回根结点
}//和普通二叉树没有什么区别
前驱:需要删除的根节点的左子树中的最右结点:即比权值小的最大结点
后继:需要删除的根节点的右子树中的最左结点:即比权值大的最小结点
做删除操作时,用这两种的某一个数据域覆盖根节点,并删除自身。
下面两个函数用来寻找以root为根的树中最大或最小权值结点,用以辅助寻找结点的前驱和后继: .
//寻找以root为根结点的树中的最大权值结点
node* findMax(node* root){
while (root->rchild != NULL) {
root = root->rchild; //不断往右, 直到没有右孩子
return root;
}
//寻找以root为根结点的树中的最小权值结点
node* findMin(node* root): {
while(root->lchild!= NULL) {
root = root->lchild;//不断往左, 直到没有左孩子
return root;
}
删除操作的基本思路如下:
①如果当前结点root为空,说明不存在权值为给定权值x的结点,直接返回。
②如果当前结点root的权值恰为给定的权值x,开始删除
a)如果当前结点root不存在左右孩子,说明是叶子结点,直接删除。
b)如果当前结点root存在左孩子,那么在左子树中寻找结点前驱pre,然后让pre的数据覆盖root,接着在左子树中删除结点pre。
c)如果当前结点root存在右孩子,那么在右子树中寻找结点后继next, 然后让next的数据覆盖root,接着在右子树中删除结点next。
③如果当前结点root的权值大于给定的权值x,则在左子树中递归删除权值为x的结点。
④如果当前结点root的权值大于给定的权值x,则在右子树中递归删除权值为x的结点。
删除操作的代码如下(如果需要,可以在删除叶子结点的同时释放它的空间):
void deleteNode(node*&root,int x) {
//删除以root为根结点的树中权值为x的结点
if (root == NULL) return;//不存在权值为x的结点
if (root -> data == x) {
// 找到欲删除结点
if (root -> lchild == NULL && root -> rchild == NULL) {
//叶子结点直接删除
root = NULL;
}//把root地址设为NULL,父结点就引用不到它了
else if (root -> lchild != NULL) {
//左子树不为空时
node * pre = findMax(root -> lchild); //找root前驱
root -> data = pre -> data;//用前驱覆盖root
deleteNode(root -> lchild, pre -> data);//在左子树中删除结点pre
} else {
//右子树不为空时
node * next = findMin(root -> rchild);// 找root后继
root -> data = next -> data; //用后继覆盖root
deleteNode(root -> rchild, next -> data); //在右子树中删除结点next
}
} else if (root -> data > x) {
deleteNode(root -> lchild, x); //在左子树中删除x
} else {
deleteNode(root -> rchild, x); //在右子树中删除x
}
}
这个有很多优化方式,只是最初的模板,之后刷leetcode时要注意
但是也要注意,总是优先删除前驱(或者后继)容易导致树的左右子树高度极度不平衡,使得二叉查找树退化成一条链。 解决这一问 题的办法有两种:
对二叉查找树进行中序遍历,遍历的结果是有序的。
AVL仍然是一颗平衡二叉树,但他所有结点左右子树的高度之差(平衡因子)的绝对值不超过1
//需要对每个结点都得到平衡因子,因此需要在树的结构中加入一一个变量height,
structnode {
int V, height;//v为结点权值,height 为当前子树高度
node *lchild, *rchild;//左右孩子结点地址
};
//在这种定义下,如果需要新建一个结点,就可以采用如下写法:
//生成一个新结点,v为结点权值
node* newNode (int v){
node*Node=new node;
//申请一个node型变量的地址空间
Node->v=v; //结点权值为v
Node->height=1;//结点高度初始为1
Node->lchild=Node->rchild=NULL;//初始状态下没有左右孩子
return Node;
//返回新建结点的地址
}
//获取以root为根结点的子树的当前height
int getHeight (node* root){
if(root==NULL)return 0;/ /空结点高度为0
return root->height;
}
//计算结点 root的平衡因子
int getBalanceFactor (node* root){
//左子树高度减右子树高度
return getHeight(root->1child)一getHeight(root->rchild);
}
//更新结点root的height
void updateHeight (node* root){
//max(左孩子的height,右孩子的height)+1
root->height.=max(getHeight(root->lchild),getHeight(root->rchild))+1;
}
包括插入、查找及建立,删除太复杂暂时不说
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kltxjrLA-1595856115301)(/…/images/%E7%AE%97%E6%B3%95%E7%AC%94%E8%AE%B0/image-20200703203200793.png)]
//左旋(Left Rotation)
/*1.让A的右子树◆成为B的左子树。
2.让B成为A的右子树。
3.将根结点设定为结点A。*/
void L(node* &root){
node*temp=root->rchild;//root指向结点A, temp 指向结点B
root->rchild=temp->lchild;//步骤1
temp->lchild=root;//步骤2
updateHeight(root);//更新结点A的高度
updateHeight(temp);//更新结点B的高度
root=temp;s //步骤3
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CBkojdN9-1595856115303)(/…/images/%E7%AE%97%E6%B3%95%E7%AC%94%E8%AE%B0/image-20200703203551838.png)]
//右旋(Right Rotation)
/*①让A的右子树◆成为B的左子树。
②让B成为A的右子树。
③将根结点设定为结点A。*/
void R (node* &root){
node*temp=root->lchild; //root 指向结点B,temp 指向结点A
root->lchild=temp->rchild;//步骤1
temp->rchild二root;//步骤2
updateHeight(root);//更新结点B的高度
updateHeight(temp);
//更新结点A的高度
root=temp;//步骤3
}
可以证明,只要把最靠近插入结点的失衡结点调整到正常,路径上的所有结点就都会平衡。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-85tqQPU0-1595856115305)(/…/images/%E7%AE%97%E6%B3%95%E7%AC%94%E8%AE%B0/image-20200703204207856.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1rtAUw7t-1595856115307)(/…/images/%E7%AE%97%E6%B3%95%E7%AC%94%E8%AE%B0/image-20200703204222130.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gbwgjaBt-1595856115308)(/…/images/%E7%AE%97%E6%B3%95%E7%AC%94%E8%AE%B0/image-20200703204252883.png)]
由于我们需要从插入的结点开始从下往上判断结点是否失衡,因此需要在每个insert函数之后更新当前子树的高度,并在这之后根据树型是LL型、LR型、RR型、RL型之一来进行平衡操作,代码如下:
//插入权值为v的结点
void insert (node* &root, int v) {
if(root = NULL){
//到达空结点
root=newNode(v);
return;
}
if(v<root->v) {
//v 比根结点的权值小
insert(root->lchild, v); //往左子树插入
updateHeight(root); //更新树高
if (getBalanceFactor(root)== 2) {
if (getBalanceFactor (root->lchild)==1) {
//LL型
R(root) ;
}else if (getBalanceFactor (root->lchild) == -1) {
//LR型
L(root->lchi1d);
R(root) ;
}
else (//v比根结点的权值大
insert (root->rchild, v);//往右子树插入
updateHeight (root);//更新树高
if (getBalanceFactor (root)==2) {
if (getBalanceFactor (root->rchild) = -1) ( //RR型
L(root);
} else if (getBalanceFactor (root->rchild) == 1){
//RL型
R(root->rchild);
L(root);
}
}
}
并查集是一种维护集合的数据结构,它的名字中“并”“查”“集”分别取自Union (合并)、Find (查找)、Set (集合)这3个单词。也就是说,并查集支持下面两个操作:
①合并:合并两个集合。
②查找:判断两个元素是否在一个集合。
实现方式:int father[N];
其中,fahter[i]表示元素i的父亲结点,而父亲结点本身也是这个集合内的元素(1≤i≤N)。例如father[1] = 2就表示元素1的父亲结点是元素2,以这种父系关系来表示元素所属的集合。
另外,如果father[i]=i,则说明元素i是该集合的根结点,但对同一个集合来说只存在-一个根结点,且将其作为所属集合的标识。
并查集产生的每一个集合都是一棵树
for(int i=1;i<=n;i++){
father[i]=i;//每个元素初始都是一个独立的集合
}
int findFather(int x){
while(x!=father(x)){
x=father[x];
}
return x;
}
先判断两个集合是否属于同一个集合
合并两个集合
void Union(int a,int b){
int faA=findFather(a);
int faB=findFather(b);
if(faA!=faB){
father[faA]=faB
}
}
上面讲的查找函数没有优化,如果查找时把当前结点路径上的所有结点的父亲都指向根结点,那之后查找就不用一直回溯了:
int findFather(int a){
if(a==father(a)) return a;
else{
int F=findFather(father(a));
father(a)=F;
return F;
}
}
堆是一棵完全二叉树,树中每个节点都不小于或不大于其左右孩子,根据每个结点的父亲结点是大还是小分为大顶堆和小顶堆。
所以和完全二叉树一样用数组存储
const int maxn=100;
int heap[maxn],n=10;//10为堆元素个数
//heap数组在[low,high]范围向下调整
//low为需要调整的数组下标,high为最后一个元素的下标
void downAdjust(int low,int high){
int i=low,j=i*2;//j为其左孩子
while(j<=high){
//存在孩子结点
if(j+1<=high&&heap[j+1]>heap[j]){
j=j+1
}
if(heap[j]>heap[i]){
//孩子比父亲大就换
swap(heap[i],heap[j]);
i=j;//保持i=欲调整结点
j=i*2;//孩子也保持
}else{
break;
}
}
}
void createHeap(){
for(int i=n/2;i>0;i++){
downAdjust(i,n);
}
}
void deleteTop(){
heap[1]=heap[n--];//用最后一个元素覆盖堆顶然后向下调整
downAdjust(1,n);
}
//向上调整,low一般设为1,high是需要调整的下标
void upAdjust(int low,int high){
int i=high,j=i/2;//和父亲比较
while(j>=low){
if(heap[j]<heap[i]){
//和父亲比较
swap(heap[j],heap[i]);
i=j;
j=i/2;
}else{
break;
}
}
}
void insert(int x){
heap[++n]=x;
upAdjust(1,n);
}
void heapSort(){
createHeap();
for(int i=n;i>1;i--){
swap(heap[i],heap[1]);
downHeap(1,i-1);
}
}//原理挺长的不写了,去看书P341
构建思想:反复选择两个最小的元素合并,直到只剩下最后一个元素
以合并果子为例
//优先队列实现
priority_queue<long long,vector<long,long>,greater<long,long> > q;
int main(){
for(...){
//将初始重量压入优先队列
q.push(temp);
}
while(q.size()>1)//至少得有俩才能合并
{
x=q.top();
q.pop();
y=q.top();
q.pop();
q.push(x+y);
ans+=x+y;
}
...
return 0;
}
即令任何一个叶子结点(字符),其编码一定不会成为其他任何一个结点(字符)编码的前缀,满足这种编码方式的编码成为前缀编码
前缀编码的存在意义在于不产生混淆,使解码正常进行。
哈夫曼编码是能使给定字符串编码成01串后长度最短的前缀编码。
由顶点Vertex和边Edge组成
name | explanation |
---|---|
有向图 | 所有边都有单个方向 |
无向图 | 所有边都是双向的 |
顶点的度 | 和该顶点相连的边数(分为入度和出度) |
权值 | (点权和边权)代表某个属性 |
——————二维数组,顶点数目比较少时用这个
//G[i][j]=1表示顶点i,j之间有边,G[i][j]=0表示顶点i,j之间无边
——————链表或者vector,顶点数目比较多时用这个
//Adj[i][0]表示当前访问的顶点标号为i,int v=Adj[i][0].v(得到可以到达的顶点标号)
Struct Node{
int v;//边编号
int w;//边权
Node(int _v,int _w):v(_v),w(_w){}//构造函数
}
vector Adj[N];
Adj[1].push_back(Node(3,4));
连通分量 | 无向图:两顶点可以相互到达,就称为连通;如果图任意两个顶点都联通,就称图为连通图;否则,称非连通图中的极大连通子图为连通分量 |
---|---|
强连通分量 | 有向图:两顶点可以各自相互到达,就称为强连通;如果图任意两个顶点都联通,就称图为强连通图;否则,称非强连通图中的极大强连通子图为强连通分量 |
DFS(u){
//访问顶点u
vis[u]=true;//设置已被访问
for(从u出发能到达的所有顶点v){
if(vis[v]==flase){
DFS(v);//递归访问v
}
}
}
DFSTrave(G){
//遍历图G
for(G的所有顶点u){
if(vis(u)==flase)
DFS(u);//访问u所在的连通块
}
}
const int MAXV=1000;//最大顶点数
const int INF=1000000000//INF设为一个很大的树
int n,G[MAXV][MAXV];//n为顶点数
bool vis[MAXV]={
false};//如果顶点已被访问,就是true
void DFS(int u,int depth){
vis[u]=true;
for(int v=0;v<n;v++){
//对所有顶点遍历一遍
if(vis[v]==flase&&G[u][v]==1){
//如果未被访问且u可以到达v
DFS(v,depth+1);//递归
}
}
}
void DFSTrave(){
//遍历图G
for(int u=0;u<n;u++){
if(vis[u]==false)
DFS(u,1)//访问u所在的连通块,1表示初始为第一层
}
}
const int MAXV=1000;//最大顶点数
vectos<int> Adj[MAXV];
int n//n为顶点数
bool vis[MAXV]={
false};//如果顶点已被访问,就是true
void DFS(int u,int depth){
vis[u]=true;
for(int i=0;v<Adj[u].size;i++){
//对所有顶点遍历一遍
int v=Adj[u][i];//u可以到达v
if(vis[v]==flase){
//如果未被访问
DFS(v,depth+1);//递归
}
}
}
void DFSTrave(){
//遍历图G
for(int u=0;u<n;u++){
if(vis[u]==false)
DFS(u,1)//访问u所在的连通块,1表示初始为第一层
}
}
BFS(u){
//访问顶点u
queue q;//定义队列q
inq[u]==true;//将顶点u入队
while(q非空){
for(从u出发能到达的所有顶点v){
if(inq[v]==flase){
//如果v没有入过队,将v入队
inq[v]=true;
}
}
}
}
BFSTrave(G){
//遍历图G
for(G的所有顶点u){
if(vis(u)==flase)
BFS(u);//访问u所在的连通块
}
}
const int MAXV=1000;//最大顶点数
int n,G[MAXV][MAXV];//n为顶点数
bool inq[MAXV]={
false};//如果顶点已入队,就是true
void BFS(int u){
queue<int> q;//定义队列q
q.push(u);
inq[u]=true;
while(!q.empty){
int u=q.front();
q.pop();//
for(int v=0;v<n;v++){
if(inq[v]==flase&&G[u][v]==1){
//如果v没有入过队且v是u的临接点,将v入队
q.push(v);
inq[v]=true;
}
}
}
}
BFSTrave(G){
//遍历图G
for(int u=0;u<n;++u){
if(inq(u)==flase)//u没入过队
BFS(u);//访问u所在的连通块
}
}
const int MAXV=1000;//最大顶点数
vectos<int> Adj[MAXV];
int n//n为顶点数
bool vis[MAXV]={
false};//如果顶点已被访问,就是true
void BFS(int u){
queue<int> q;//定义队列q
q.push(u);
inq[u]=true;
while(!q.empty){
int u=q.front();
q.pop();//
for(int i=0;i<Adj[u].size;i++){
int v=Adj[u][i];
if(inq[v]==flase){
//如果v没有入过队且v是u的临接点,将v入队
q.push(v);
inq[v]=true;
}
}
}
}
void BFSTrave(){
//遍历图G
for(int u=0;u<n;u++){
if(vis[u]==false)
DFS(u)//访问u所在的连通块,1表示初始为第一层
}
}
//要求层号就用结构体,加点代码就行
作用:解决单源最短路径问题(边权均为非负数)
伪代码
//G为图,一般设置成全局变量;数组d为源点到达各点最短路径长度;s为起点
Dijkstra(G,d[],s){
初始化;
for(循环n次){
for(){
u=使d[u]最小的还未访问的顶点;
}
标记u被访问;
for(u出发可以访问的所有顶点v){
if(v未被访问&&以u到达v的路径可以使d[v]更优)
优化d[v];
}
}
}
const int MAXV=1000;//最大顶点数
const int INF=1000000000;//INF为一个很大的数
int n,G[MAXV][MAXV];//n为顶点数
int d[MAXV];
bool vis[MAXV]={
false};//如果顶点已入队,就是true
//G为图,一般设置成全局变量;数组d为源点到达各点最短路径长度;s为起点
void Dijkstra(int s){
//初始化;
fill(d,d+MAXV,INF);//fill给整个d数组复制,慎用memset数组
d[s]=0;//起点s到达自己的距离为0
for(int i=0;i<n;i++){
int u=-1,MIN=INF;//u使d[u]最小,MIN存最小d[u]值;
for(int j=0;j<n;j++){
//循环n次找到未访问的顶点;
if(vis[j]==flase&&d[j]<MIN){
u=j;//u=使d[u]最小的还未访问的顶点;
MIN=d[j];
}
}
if(u==-1) return;//找不到小于INF的d[u],说明剩下的顶点和起点s不流通;
vis[u]=true;// 标记u被访问;
for(int v=0;v<n;v++){
if(vis[v]==flase&&G[u][v]!=INF&&d[u]+G[u][v]<d[v])
d[v]=d[u]+G[u][v];//优化d[v];
}
}
}
const int MAXV=1000;//最大顶点数
struct Node{
int v,dis;//v=目标顶点;dis=边权
}
vectos<Node> Adj[MAXV];
int n//n为顶点数
bool vis[MAXV]={
false};//如果顶点已被访问,就是true
int d[MAXV];
//G为图,一般设置成全局变量;数组d为源点到达各点最短路径长度;s为起点
void Dijkstra(int s){
//初始化;
fill(d,d+MAXV,INF);//fill给整个d数组复制,慎用memset数组
d[s]=0;//起点s到达自己的距离为0
for(int i=0;i<n;i++){
int u=-1,MIN=INF;//u使d[u]最小,MIN存最小d[u]值;
for(int j=0;j<n;j++){
//循环n次找到未访问的顶点;
if(vis[j]==flase&&d[j]<MIN){
u=j;//u=使d[u]最小的还未访问的顶点;
MIN=d[j];
}
}
if(u==-1) return;//找不到小于INF的d[u],说明剩下的顶点和起点s不流通;
vis[u]==true;// 标记u被访问;
for(int j=0;j<Adj[u].size();j++){
int v=Adj[u][j].v;
if(vis[v]==flase&&d[u]+Adj[u][j].dis<d[v])
d[v]=d[u]+Adj[u][j].dis;//优化d[v];
}
}
}
——解决单源路径最短且有负边权
还没看懂原理,之后刷到这种题就重新回P393看一遍
——全源最短路径,即找到任意两点之间最短的路径
枚举顶点k属于[1,n]
以顶点k作为中介点,枚举所有顶点对i和j(i属于[1,n],j属于[1,n]
如果dis[i][k]+dis[k][j]<dis[i][j]成立
赋值dis[i][j]=dis[i][k]+dis[k][j]
void Floyd(){
for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
for(int j=0;j<n;j++){
if(dis[i][k]!=INF&&dis[k][j]!=INF&&dis[i][k]+dis[k][j]<dis[i][j])
dis[i][j]=dis[i][k]+dis[k][j]
}
}
最小生成树拥有无向图的所有顶点且满足整棵树的边权之和最小
下面两种算法都采用了贪心法,只是贪心的策略不太一样;
稠密图(边多)用prim算法;稀疏图(边少)用krustal算法
——解决最小生成树问题
思想和Dijkstra几乎完全相同,区别仅在于d[]的含义不同//ans记录了最小生成树的总路径
//G为图,一般设置成全局变量;数组d为源点到达各点最短路径长度;s为起点
Prim(G,d[]){
初始化;
for(循环n次){
for(){
u=使d[u]最小的还未访问的顶点;
}
标记u被访问;
for(u出发可以访问的所有顶点v){
if(v未被访问&&以u为中介点使得v与集合S的最短路径可以使d[v]更优)
将G[u][v]赋值给v与集合S的最短路径d[v];
}
}
}
const int MAXV=1000;//最大顶点数
const int INF=1000000000;//INF为一个很大的数
int n,G[MAXV][MAXV];//n为顶点数
int d[MAXV];//顶点与集合的最短距离
bool vis[MAXV]={
false};//如果顶点已访问,就是true
//G为图,一般设置成全局变量;数组d为源点到达各点最短路径长度;s为起点
int Prim(){
//初始化;
fill(d,d+MAXV,INF);//fill给整个d数组复制,慎用memset数组
d[s]=0;//起点s到达集合S的距离为0
int ans=0;//存放最小生成树的边权之和
for(int i=0;i<n;i++){
int u=-1,MIN=INF;//u使d[u]最小,MIN存最小d[u]值;
for(int j=0;j<n;j++){
//循环n次找到未访问的顶点;
if(vis[j]==flase&&d[j]<MIN){
u=j;//u=使d[u]最小的还未访问的顶点;
MIN=d[j];
}
}
if(u==-1) return -1;//找不到小于INF的d[u],说明剩下的顶点和起点s不流通;
vis[u]=true;// 标记u被访问;
ans+=d[u];//将于集合S距离最小的边加入最小生成树
for(int v=0;v<n;v++){
if(vis[v]==flase&&G[u][v]!=INF&&G[u][v]<d[v])
d[v]=G[u][v];//优化d[v];
}
}
return ans;
}
const int MAXV=1000;//最大顶点数
struct Node{
int v,dis;//v=目标顶点;dis=边权
}
vectos<Node> Adj[MAXV];
int n//n为顶点数
bool vis[MAXV]={
false};//如果顶点已被访问,就是true
int d[MAXV];
//G为图,一般设置成全局变量;数组d为源点到达各点最短路径长度;s为起点
int Prim(){
//初始化;
fill(d,d+MAXV,INF);//fill给整个d数组复制,慎用memset数组
d[s]=0;//起点s到达集合S的距离为0
int ans=0;//存放最小生成树的边权之和
for(int i=0;i<n;i++){
int u=-1,MIN=INF;//u使d[u]最小,MIN存最小d[u]值;
for(int j=0;j<n;j++){
//循环n次找到未访问的顶点;
if(vis[j]==flase&&d[j]<MIN){
u=j;//u=使d[u]最小的还未访问的顶点;
MIN=d[j];
}
}
if(u==-1) return -1;//找不到小于INF的d[u],说明剩下的顶点和起点s不流通;
vis[u]=true;// 标记u被访问;
ans+=d[u];//将于集合S距离最小的边加入最小生成树
for(int v=0;v<n;v++){
int v=Adj[u][j].v;
if(vis[v]==flase&&Adj[u][j].dis<d[v])
d[v]=Adj[u][j].dis;//优化d[v];
}
}
return ans;
}
int kruskal(){
令最小生成树的边权之和为ans,最小生成树的当前边数Num_Edge
将所有边按边权从大到小排序;
for(从大到小枚举所有边){
if(当前测试边的两个端点在不同的连通块中){
将该测试边加入最小生成树中;
ans+=测试边的边权;
最小生成树的当前边数Num_Edge+1;
当Num_Edge=顶点数-1时结束循环;
}
}
return ans;
}
判断两个端点是否在一个连通块中的方法是使用并查集
struct edge{
int u,v;//边的两个端点
int cost;//边权
}E[MAXE];//最多MAXE条边
bool cmp(edge a,rdge b){
return a.cost<b.cost;//从小到大对边权排序
}
int father[N];//并查集数组
int findFather(int x){
//并查集查询函数
while(x!=father(x)){
x=father[x];
}
return x;
}
//krustal函数返回最小生成树的边权之和,参数n为顶点个数,m为图的边数
int krustal(int n,int m){
//令最小生成树的边权之和为ans,最小生成树的当前边数Num_Edge
int ans=0,Num_edg=0;
for(int i=0;i<=n;i++){
father[i]=i;//并查集初始化
}
sort(E,E+m,cmp);//对边排序
for(int i=0;i<m;i++){
//枚举所有边
int faU=findFather(E[i].u);
int faV=findFather(E[i].v);
if(faU!=faV){
father[faU]=faV;//合并集合
ans+=E[i].cost;
Num_edge++;
if(Num_edge==n-1) break;
}
}
if(Num_edge!=n-1) return -1;//最终也无法联通就返回-1
else return ans;
}
如果一个有向图的任意顶点都无法通过一些有向边回到自身,这个图被称为有向无环图DAG
拓扑排序是将有向无环图G的所有顶点排成一个线性序列,使得图G中的任意两个点u,v,如果存在边u->v,那么在序列中u一定在v的前面。这个序列就叫做拓扑序列。
1、定义队列q,所有入度为0的顶点加入队列;
2、输出队首结点,删除所有由它出发的边,并且令对应的顶点入度减一,如果这是某个顶点入度=0,就将此顶点加入队列;
3、反复2操作,直到队列为空,此时如果入过队的结点刚好=N,则拓扑排序成功,图是DAG,否则说明存在闭环
vector<int> G[MAXV];//邻接表
int n,m,inDegree[MAXV];
bool topologicalSort(){
int num=0;//记录加入拓扑排序的顶点数
queue<int> q;
for(int i=0;i<n;i++){
if(inDegree[i]==0){
q.push(i);//所有入度为0的顶点加入队列;
}
}
while(!q.empty){
int u=q.front();
q.pop();
for(int i=0;i<G[u].size;i++){
int v=G[u][i].v;
inDegree[v]--;
if(inDegree[v]==0){
q.push(v);
}
}
G[u].clear();//清空u的所有出边(不必要可以不写)
num++;
}
if(num==n)
return true;
else
return false;
}
AOV网 | Acticity On Vertex——顶点表示活动,而用边集表示活动间优先关系的有向图; |
---|---|
AOE网 | Activity On Edge——带圈的边集表示活动,顶点表示事件的有向图; |
源点 | 入度为0的点(或者创建一个新点连接所有入度为0的点) |
汇点 | 出度为0的点(或者创建一个新点连接所有出度为0的点) |
AOE网的最长路径就是关键路径,关键路径上的活动称为关键活动,所需时间就是最短时间
AOV转换为AOE的方法就是拆解顶点,边视为空活动。
对于一个没有正环的图,求最长路径,就所有边乘以-1再用Bellman-Ford算法或者SPFA算法求最短路径再取反。
AOE是有向无环图,所以这里的方法实际上是求解有向无环图DAG中最长路径的方法。
如下——活动示意图:
事 件 V i − − − − − − − − − − − − − − − − − 活 动 a r − − − − − − − − − − − − − − − − − − − − 事 件 V j 事件V_i-----------------活动a_r--------------------事件V_j 事件Vi−−−−−−−−−−−−−−−−−活动ar−−−−−−−−−−−−−−−−−−−−事件Vj
由于关键活动是那些不允许拖延的活动,因此这些活动的最早开始时间必须等于最迟开始时间。因此可以设置数组e和l,其中e[r]和l[r]分别表示活动a_r的最早开始时间和最迟开始时间。于是,当我们求出这两个数组之后,就可以通过判断e[r]==l[r]是否成立来确定活动r是否是关键活动。
那么,如何求解数组e和l?
我们看上面的活动示意图可以知道——
**事件最早发生时间旧活动的最早结束时间,事件的最迟发生时间新活动的最迟开始时间。?(事件的最迟发生时间==旧活动的最迟结束时间。)**所以,可以再设置数组ve[i],vl[i]分别表示事件i的最早发生时间和最晚发生时间,然后我们就可以根据下面的公式转换得出e[r]和l[r]了。
ps:事件可以拖延,所以没有结束时间。
1、事件Vi最早发生时间=新活动a_r最早开始时间
e[r]=ve[i];
2、事件Vj最晚发生时间-length[r]=活动a_r最晚开始时间
l[r]=vl[j]-length[r];
3、假设已知k个事件V_i1~~V_ik的最早发生时间,要求事件V_j的最早发生时间,此时由于只有所有事件都到达后,事件j才能开始,所以取最大值
ve[j]=max{
ve[ip]+length[rp]}
4、假设已知k个事件V_j1~~V_jk的最晚发生时间,要求事件V_i的最晚发生时间,此时由于必须保证所有后续结点的最晚到达时间都能被满足,所以取最小值
vl[i]=min{
vl[jp]-length[rp]}
5、用上面的结果计算各边的结果
最早:e[i->j]=ve[i]
最晚:l[i->j]=vl[j]-length[i->j]
主题代码如下:
------------------------------------------------------------------------------------------
已知所有前驱结点的ve,求结点的ve:-------------------------------------------------------------
int n,m,inDegree[MAXV];
//拓扑序列
stack<int> topOrder;
//拓扑序列排序,顺便先求ve数组(每个事件的最早发生时间是根据前一个事件的最早发生时间+边的长度得出的。
bool topologicalSort(){
queue<int> q;
for(int i=0;i<n;i++){
if(inDegree[i]==0){
q.push(i);//所有入度为0的顶点加入队列;
}
}
while(!q.empty){
int u=q.front();
q.pop();
topOrder.push(u);//将u加入拓扑序列
for(int i=0;i<G[u].size;i++){
int v=G[u][i].v;
inDegree[v]--;
if(inDegree[v]==0){
q.push(v);
}
}
//用ve[u]来更新u的所有后继结点v
if(ve[u]+G[u][i].w>ve[v]){
ve[v]=ve[u]+G[u][i].w;
}
}
if(topOrder.size()==n) return true;
else return false;
}
------------------------------------------------------------------------------------------
已知所有后继结点的vl,求结点的vl--------------------------------------------------------------
适用于汇点确定且唯一的情况,以n-1为汇点为例
//关键路径,不是有向无环图返回-1,否则返回关键路径长度,顺便把vl数组的值求了
int CriticalPath(){
memset(ve,0,sizeof(ve));//ve数组初始化
if(topologicalSort==false) return -1;//不是有向无环图,退出
fill(vl,vl+n,ve[n-1]);//vl数组初始化,初始值为汇点的ve值
//直接使用topOrder出栈即为逆拓扑排序,求解vl数组
while(!topOrder.empty()){
int u=topOrder.top();
topOrder.pop();
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v;//u的后继结点v
//用u的所有后继节点v的vl值来更新vl[u]
if(vl[v]-G[u][i].w<vl[u]){
vl[u]=vl[v]-G[u][i].w;
}
}
}
//遍历邻接表的所有边,计算活动的最早活动开始时间e和最迟开始时间l
for(int u=0;u<n;u++){
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v,w=G[u][i].w;
int e=ve[u],l=vl[v]-w;
//如果e==1,说明活动u->v是关键活动
if(e==l){
printf("%d->%d\n",u,v);//输出关键活动
}
}
}
return ve[n-1];//返回关键路径长度
}
------------------------------------------------------------------------------------------
ps:
1、如果实现不知道汇点编号,就取ve数组的最大值,因为ve最大的肯定是最后一个
2、如果想要存下关键活动并输出,就设置一个邻接表存一下就好。
动态规划是一种用来解决一类最优化问题的算法思想,简单来说,就是将一个复杂的问题分解为若干个子问题,通过综合子问题的最优解来得到原问题的最优解,这个过程中动态规划会把每个子问题的最优解记录下来。
实现方式有递归(例如斐波那契数列)和递推(树塔问题)
注意:一个问题必须拥有重叠子问题和最优子结构才能用DP
问题描述:给定一个数字序列A1~An,求i,j(1<=i<=j),使得A1+…+Aj最大,输出这个最大和
解决步骤:
- 步骤一:令状态dp[i]表示以A[i]作为末尾的连续序列的最大和
代码实现
#include
#include
using namespace std;
const int maxn =10010;
int A[maxn],dp[maxn];
int main(){
int n;
scanf("d%",&n)
for(int i=0;i
问题描述:Longest Increasing Sequence,在一个数字序列A1~An,中,找到一个最长的子序列(可以不连续),使得这个子序列是不下降(非递减)的。
解决步骤:
代码实现
const int maxn =100;
int A[maxn],dp[maxn];
int main(){
int n;
scanf("d%",&n)
for(int i=1;i<=n;i++){
scanf("d%",&A[i])
}
int ans=-1;//记录最大的dp[i]
for(int i=1;i<=n;i++){
//边界
dp[i]=1;
for(int j=1;j<i;j++){
if(A[j]<A[i]&&(dp[i]<dp[j]+1))
dp[i]=dp[j]+1;
}
ans=max(ans,dp[i]);
}
printf("d%",ans);
return 0;
}
问题描述:Longest Common Sequence,在两个字符串(数字)序列A1~An,中,找到最长的公共的的子序列(可以不连续)
解决步骤:
代码实现
const int N =100;
int A[N],B[N];
int dp[N[N];
int main(){
int n;
gets(A+1);//从下标为1开始读入
gets(B+1);
int lenA=strlen(A+1);
int lenB=strlen(B+1);
//边界
for(int i=1;i<=n;i++){
d[i][0]=0;
}
for(int i=1;i<=n;i++){
d[0][i]=0;
}
for(int i=1;i<=lenA;i++){
for(int j=1;j<lenB;j++){
if(A[i]==B[i]){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1])
}
}
}
printf("d%\n",dp[lenA][lenB]);
return 0;
}
#include
#include
#include
#include
#include
#include
using namespace std;
string lower(string s)
{
for (int i = 0; i < s.size(); i++) //把所有字符变成小写
{
if (s[i] >= 'A' && s[i] <= 'Z')
s[i] = s[i] + 32;
}
return s;
}
int main(int argc,char *argv[]){
string str1;
string str2;
while(cin>>str1>>str2){
lower(str1);
lower(str2);
int len1=str1.length();
int len2=str2.length();
int flag=0,max=0;
int dp[len1+1][len2+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<len1+1;i++)
for(int j=1;j<len2+1;j++){
if(str1[i-1]==str2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
if(max<dp[i][j])
max=dp[i][j];
}
else
dp[i][j]=0;
}
printf("%d\n",max);
}
return 0;
}
- 一、求整个DAG中的最长路径(不固定起点和终点)
- 二、固定终点,求DAG的最长路径
先讨论第一个问题,给定一个DAG,怎样求解整个图里所有路径中权值之和最大的那条?
解决步骤:
int DP(int i){
if(dp[i]>0) return dp[i];
for(int j=0;j<n;j++)
if(G[i][j]!=INF){
dp[i]=max(dp[i],DP(j)+length[i->j]);
}
return dp[i];
}
//如果想存下关键路径,开个choice数组记录每个结点的后继节点。
int DP(int i){
if(vis[i]) return dp[i];
vis[i]=true;
for(int j=0;j<n;j++)
if(G[i][j]!=INF){
dp[i]=max(dp[i],DP(j)+length[i->j]);
}
return dp[i];
}
ps:矩形嵌套问题就是一个比较经典的DP问题
是一类经典的动态规划问题———比如———01背包问题和完全背包问题
一个问题分为多个阶段,且每个阶段的状态只和上一个阶段的状态有关
步骤一:令状态dp[i] [v] 表示前i件物品恰好装入容量为v时所装入所有物品的价值
步骤二:根据dp[i]的要求我们可以知道
for(int i=1;i<=n;i++){
for(int v=w[i];v<=V;v++){
dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+c[i])
}
}
步骤一:令状态dp[i] [v] 表示前i件物品恰好装入容量为v时所装入所有物品的价值
步骤二:根据dp[i]的要求我们可以知道
区别:
放入i物品时转换的是 : dp[i] [v-w[i]]+c[i]
而不是之前的 : dp[i-1] [v-w[i]]+c[i]了
for(int i=1;i<=n;i++){
for(int v=w[i];v<=V;v++){
dp[i][v]=max(dp[i-1][v],dp[i][v-w[i]]+c[i])
}
}
名称 | dp数组含义 |
---|---|
1.最大连续子序列和 | 令dp[i]表示以A【i】作为结尾的连续序列的最大和 |
2.最长不下降子序列(LIS) | 令dp[i] 表示以A【i】作为结尾的最长不下降子序列长度 |
3.最长公共子序列(LCS) | 令dp[i] [j]表示以序列A的i号位和序列B的j号位作为末尾的最长公共子序列的长度 |
4.最长回文子串 | 令dp [i] [j]表示A[i]到A[j]是否是回文子串 |
5.数塔DP | 令dp [i] [j]表示从i行j列数字出发的到达最底层的所有路径上所能得到的最大和 |
6.DAG最长路 | 令dp [i] 表示从i顶点出发能获得的最长路径长度 |
7.01背包 | 令dp[i] [v] 表示前i件物品恰好装入容量为v时所装入所有物品的最大价值 |
8.完全背包 | 令dp[i] [v] 表示前i件物品恰好装入容量为v时所装入所有物品的最大价值 |