第四章 算法初步
4.1 排序
4.1.1 选择排序
对一个序列A中的元素A[1] - A[n]
,令i
从1
到n
枚举,进行n
趟操作,每趟操作从待排序部分[i,n]
中选择最小的元素,令其与待排序部分的第一个元素A[i]
交换,从而形成新的已排序部分[1,i]
。
void selectSort()
{
for(int i=1;i<=n;i++)
{
int k=i;
for(int j=i;j<=n;j++)
{
if(a[k]<a[j]) //最小元素
k=j;
}
swap(a[i],a[k]);
}
}
4.1.2 插入排序
对一个序列A中的元素A[1] - A[n]
,令i从2到n枚举,进行n-1趟操作,每趟操作在已排序部分[1,i-1]
中寻找元素A[i]
的位置j
,[j,i-1]
的元素要移到[j+1,i]
,再插入元素A[i]
,从而使得[1,i]
有序。
void insertSort()
{
for(int i=2;i<=n;i++)
{
int tmp=a[i],j=i;
while(j>1&&tmp<a[j-1]) //和前一个元素比较,一边比较一边移动
{
a[j]=a[j-1];
j--;
}
a[j]=tmp;
}
}
4.1.3 排序题与sort函数的应用
strcmp
strcmp(string a,string b);
若a<b
,返回一个负数;若a==b
,返回0;若a>b
,返回一个正数。
【注】 返回的不一定是-1和1。
计算排名
从第二个开始遍历。
stu[0].r=1;
for(int i=1;i<n;i++) //从第二个开始遍历
{
if(stu[i].score==stu[i-1].score) //与前一个比较
stu[i].r=stu[i-1].r;
else
stu[i].r=i+1;
}
直接输出
int r=1; //排名
for(int i=0;i<n;i++)
{ //当前个体不是第一个且不等于上一个个体的分数时,排名更新
if(i>0&&stu[i].score!=stu[i-1].score)
r=i+1;
cout<<r;
}
4.2 散列
4.2.1 散列
空间换时间。
1. 常用的散列函数
H(key)=key
H(key)=key%mod
当mod是一个素数时,H(key)能尽可能覆盖[0,mod)范围内的每一个数。
2. 冲突处理方法
ATTENTION
- 取模!!!
- 元素的查找次数=冲突次数+1
2.1 线性探查法 Linear Probing
如果H(key)
已被使用,查看H(key)+1
是否被使用。如果超过了表长,就返回表首继续循环,直到找到一个可用的位置,或者发现表中所有位置都已被使用。
容易扎堆。
2.2 平方探查法 Quadratic Probing
H(key)+12,H(key)-12,H(key)+22,H(key)-22,H(key)+32,…
如果 H(key)+k2 超过了表长TSize,则将H(key)+k2对TSize取模。如果H(key)-k2<0(假设表首位为0),则将 ((H(key)-k2)%TSize+TSize)%TSize 作为结果(即将 H(key)-k2 不断加上TSize直到出现第一个非负数)。
有定理显示:如果散列表长度Size是某个4k+3形式的素数时,平方探测法可以探测到整个散列表空间。
【注】可以证明,如果k在[0,TSize)范围内无法找到位置,则当k>=TSize时,也一定无法找到位置。
2.3 链地址法(拉链法)
把所有H(key)
相同的key
连接成一条单链表。
4.2.2 字符串hash初步
字符串hash
是将一个字符串S
映射为一个整数,使得该整数尽可能唯一地代表字符串S。
假设字符串均由大写字母A~Z构成,则使用26进制,再转换为十进制。
int hashTable[26*26*26+10];
int hashFunc(char s[],int len)
{
int id=0;
for(int i=0;i<len;i++)
{
id=id*26+(s[i]-'A'); //26进制转换为十进制
}
return id;
}
- 如果出现了小写字母
则把A ~ Z作为0 ~ 25,把a ~ z作为26 ~ 51,即看做52进制转十进制。 - 如果出现了数字
1.增大进制。
2.如果保证在字符串的末尾是确定的数字,把前面的英文字母转换为数字后,再将末尾的数字直接拼上去。
int hashFunc(char s[],int len)
{
int id=0;
for(int i=0;i<len-1;i++)
{
id=id*26+(s[i]-'A'); //26进制转换为十进制
}
id=id*10+s[len-1]-'0';
return id;
}
ATTENTION
- 要注意字符串的长度,如果字符串的长度太长,会超过
int
的范围。
4.3 递归
4.3.1 分治
将原问题划分成若干个规模较小而结构与原问题相同或相似的子问题,然后分别解决这些子问题,最后合并子问题的解,得到最终的答案。分治分解出的子问题应当是相互独立、没有交叉的。
4.3.2 递归
递归边界+递归调用
【注】递归边界一定要记得return!!!
n的阶乘
int f(int n)
{
if(n==0) return 1;
else return f(n-1)*n;
}
斐波那契数列
int f(int n)
{
if(n==0||n==1) return 1;
else return f(n-1)*f(n-2);
}
全排列
const int maxn=11;
int n,p[maxn],hashTable[maxn]={false};
void generateP(int idx) //idx表示第idx位数字 1-n ; n+1表示结束
{
if(idx==n+1)
{
for(int i=1;i<=n;i++)
printf("%d ",p[i]);
printf("\n");
return ;
}
for(int x=1;x<=n;x++)
{
if(hashTable[x]==false) //这个数字没有被使用过
{
p[idx]=x;
hashTable[x]=true; //很重要!!!
generateP(idx+1);
hashTable[x]=false; //很重要!!!
}
}
}
n皇后问题
const int maxn=11;
int n,p[maxn],hashTable[maxn]={false};
int cnt=0; //方案数
void generateP(int idx) //idx表示第idx位数字 1-n ; n+1表示结束
{
if(idx==n+1)
{
bool flag=true;
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
if(abs(i-j)==abs(p[i]-p[j])) //在同一对角线上
flag=false; //不合法
}
if(flag) cnt++;
return ;
}
for(int x=1;x<=n;x++)
{
if(hashTable[x]==false) //这个数字没有被使用过
{
p[idx]=x;
hashTable[x]=true;
generateP(idx+1);
hashTable[x]=false;
}
}
}
优化后的n皇后
const int maxn=11;
int n,p[maxn],hashTable[maxn]={false};
int cnt=0; //方案数
void generateP(int idx) //idx表示第idx位数字 1-n ; n+1表示结束
{
if(idx==n+1)
{
cnt++;
return ;
}
for(int x=1;x<=n;x++)
{
if(hashTable[x]==false) //这个数字没有被使用过
{
bool flag=true;
for(int pre=1;pre<idx;pre++) //将要加入的这个皇后与其他皇后冲突吗!
{
if(abs(idx-pre)==abs(x-p[pre])) //冲突!不继续递归该种情况
{
flag=false;
break;
}
}
if(flag) //不冲突,继续递归
{
p[idx]=x;
hashTable[x]=true;
generateP(idx+1);
hashTable[x]=false;
}
}
}
}
4.4 贪心
4.4.1 简单贪心
贪心:考虑在当前状态下的局部最优策略,从而使全局结果最优。一般使用反证法和数学归纳法证明。
一般的,如果在想到某个似乎可行的策略,并且自己无法举出反例时,就实现它。
4.4.2 区间贪心
区间不相交问题:给出N
个开区间(x,y)
,从中选择尽可能多的开区间,使得这些开区间两两没有交集。
- 把所有的开区间左端点
x
从小到大排序。 - 如果去除区间包含的情况,则一定有**y1>y2>…>yn**成立。
- 如果是区间包含的情况,为了有更少的交集,肯定是选择左端点更大的区间。对于没有区间包含的情况,I1的右边会有一段是一定不会和其他区间重叠的,如果去除这一段,则I1将会被I2包含,所以应当选择I1。因此,每次总是选择左端点最大的区间。
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=110;
struct inteval{
int x,y;
}I[maxn];
bool cmp(inteval &a,inteval &b) //按照左端点从小到大排序
{
if(a.x!=b.x) return a.x>b.x;
else return a.y>b.y; //左端点相同的,按右端点有小到大排序。
}
int main()
{
int n;
while(scanf("%d",&n),n!=0)
{
for(int i=0;i<n;i++)
{
scanf("%d%d",&I[i].x,&I[i].y);
}
sort(I,I+n,cmp);
//ans记录不相交的区间个数,lastX记录上一个被选择的区间的左端点
int ans=1,lastX=I[0].x; //第一个默认被选择
for(int i=1;i<n;i++) //剩下的n-1个区间
{
if(I[i].y<=lastX) //开区间,可以等于
{
lastX=I[i].x;
ans++;
}
}
printf("%d\n",ans);
}
return 0;
}
区间选点问题:给出N
个闭区间[x,y]
,求最少需要确定多少个点,才能使得每个闭区间都至少存在一个点。
- 如果是区间包含的情况,假设I1被I2包含,则一定是在I1中选点。
- 对于没有区间包含的情况,对于左端点最大的区间来说,选择左端点可以包括做多的集合。因此,每次选择左端点最大的区间。
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=110;
struct inteval{
int x,y;
}I[maxn];
bool cmp(inteval &a,inteval &b) //按照左端点从小到大排序
{
if(a.x!=b.x) return a.x>b.x;
else return a.y>b.y; //左端点相同的,按右端点有小到大排序。
}
int main()
{
int n;
while(scanf("%d",&n),n!=0)
{
for(int i=0;i<n;i++)
{
scanf("%d%d",&I[i].x,&I[i].y);
}
sort(I,I+n,cmp);
//ans记录点的个数,lastX记录上一个被选择的区间的左端点
int ans=1,lastX=I[0].x; //第一个默认被选择
for(int i=1;i<n;i++) //剩下的n-1个区间
{
if(I[i].y<lastX) //闭区间
{
lastX=I[i].x; //每次选择左端点
ans++;
}
}
printf("%d\n",ans);
}
return 0;
}
4.5 二分
4.5 1 二分查找
//序列必须严格递增!
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;
}
【注】如果left+right可能超过int,则使用mid=left+(right-left)/2
代替。
如果元素可能重复,求出序列中第一个大于等于x的元素位置L以及第一个大于x的元素位置R,则x在序列中的存在区间就是[L,R)。
int lower_bound(int a[],int left,int right,int x) //第一个小于等于x的数的位置
{
int mid;
while(left<right)
{
mid=(left+right)/2;
if(a[mid]>=x)
right=mid;
else
left=mid+1;
}
return left;
}
int upper_bound(int a[],int left,int right,int x) //第一个大于x的数的位置
{
int mid;
while(left<right)
{
mid=(left+right)/2;
if(a[mid]>x)
right=mid;
else
left=mid+1;
}
return left;
}
4.6 two pointers
双重循环时,复杂度为O(n2),当n
的规模为**105**时,容易tle
。
但是使用two pointers就可以将复杂度降为O(n)。
4.6.1 two pointers
1. 两数之和
假设序列递增,求取a[i]
和a[j]
,使两者之和为M
。
- 另
i
和j
分别指向第一个元素和最后一个元素。 - 根据
a[i]+a[j]
与M
的大小关系进行处理:
a[i]+a[j]=M
。已找到一组复合条件的方案。剩余方案只可能在[i+1,j-1]
之间产生,所以另i=i+1
,j=j-1
。
a[i]+a[j]>M
。剩余方案应该在[i,j-1]
之间产生。另j=j-1
。
a[i]+a[j]<M
。剩余方案应该在[i+1,j]
之间产生。另i=i+1
。 - 反复执行,直到
i>=j
。
while(i<j)
{
if(a[i]+a[j]==m)
{
i++;
j--;
}
else if(a[i]+a[j]>m)
j--;
else
i++;
}
复杂度:最坏情况i>=j
时移动结束,则最多移动n
次,复杂度为O(n)
。
2. 序列合并
建设两个递增序列A
与B
,将他们合并成一个递增序列C
。
- 另
i
和j
分别指向序列A的第一个元素和序列B的第一个元素。 - 根据
A[i]
和B[j]
的大小关系进行处理:
A[i]>B[j]
。说明B[j]
是较小的那一个元素,将B[j]
加入序列C
中,然后j++
。
A[i]<B[j]
。说明A[i]
是较小的那一个元素,将A[i]
加入序列C
中,然后i++
。
A[i]==B[j]
。任选一个加入序列C
,并将对应的下标++
。 - 反复执行,直到有一个达到序列末端。最后将剩下的那个序列的所有元素加入到序列
C
中。
int merge(int a[],int b[],int c[],int n,int m)
{
int i=0,j=0,idx=0;
while(i<n&&j<m)
{
if(a[i]>=b[j])
c[idx++]=b[j++];
else
c[idx++]=a[i++];
}
while(i<n)
c[idx++]=a[i++];
while(j<m)
c[idx++]=b[j++];
return idx;
}
4.6.2 归并排序
4.6.3 快速排序
4.7 其他高效技巧与算法
4.7.1 打表
- 在程序中一次性计算出所有需要用到的结果,之后的查询直接取用结果。
- 在程序B中分一次或多级计算出所需要的结果,手工把结果写在程序A的数组中,在程序A中直接使用结果。
e.g. 在本地计算出n皇后的方案数,把结果直接写入数组。(妙啊) - 对于一些不会做的题,可以先暴力计算处小范围数据的结果,然后找规律,也许能找到方法。
4.7.2 活用递推
递推!
e.g. 1093(数PAT),1003(数1)…