2.6
2.6.1
1.求最大公约数
例题:线段上格点的个数
枚举的话的时间复杂度为O(|x2-x1| x |y2-y1|),对坐标的绝对值较大的情况难以处理。其实这道题的答案如page114的图所示,是|x1-x2|和|y1-y2|的最大公约数-1(要注意特判|x1-x2=0且|y1-y2|=0时答案为0).
辗转相除法:设gcd(a,b)是设计子认输a和b的最大公约数的函数,a除b得到的商和余数分别为p和q。因为a=bxp+q,所以gcd(b,q)即整除a又整除b,也就整除gcd(a,b)。反之,因为q=a-bxp,同理可证gcd(a,b)整除gcd(b,q)。因此可以知道gcd(a,b)=gcd(b,a%b)。这样不断操作下去,由于gcd的第二个数总是不断减小的最终会得到gcd(a,b)=gcd(c,0)。0和c的最大公约数为c。
int gcd(int a,int b){
if(b==0)return a;
return gcd(b,a%b);
}
复杂度为O(log max(a,b))以内。
例题:双六(扩展欧几里得算法)
这个问题用数学语言表述就是“求整数x和y使得ax+by=1”.可以发现,如果gcd(a,b)!=1,显然无解。反之,如果gcd(a,b)=1,就可以通过扩展原来的辗转相除法来求解事实上,一定存在一堆整数(x,y)使得ax+by=gcd(a,b),并且可以用同样的方法求得。
设int extgod(int a,int b,int & x,int & y)是求解该方程的函数,其返回值是gcd(a,b)。与gcd一样,我们可以递归地定义extgcd。假设已经求得了:
bx’+(a%b)y’=gcd(a,b)
的整数解x’和y’。再将
a%b=a-(a/b)xb
代入后就得到
ay’+b(x’-(a/b)xy’)=gcd(a,b)
而当b=0时则有
ax1+bx0=a=gcd(a,b)
将上述数学语言转换成代码后,就得到如下程序
**扩展欧几里得算法:对于不完全为 0 的非负整数 a,b,若gcd(a,b)表示 a,b 的最大公约数,必然存在整数对x,y ,使得 ax+by = gcd(a,b)。
算法过程:
设 a>b,当 b=0时,gcd(a,b)=a。此时满足ax+by = gcd(a,b)的一组整数解为x=1,y=0;当a*b!=0 时,
设 ax1+by1=gcd(a,b);b*x2+(a mod b)*y2=gcd(b,a mod b);
根据欧几里得原理知 gcd(a,b)=gcd(b,a mod b);则:ax1+by1=b*x2+(a mod b)*y2;
即:ax1+by1=b*x2+(a-(a/b)b)y2=ay2+bx2-(a/b)by2;(a/b:表示a除以b的商)
根据恒等定理得:x1=y2; y1=x2-(a/b)*y2;
按照上面的递归思想,我们不断对gcd进行递归,一定会出现b=0的情况,此时x=1,y=2,递归结束。这样我们就得到了求解一组x1,y1 的方法
int x,y,r;
void extgcd(int a,int b){
if(b==0){
x=1;
y=0;
r=a;
}else{
extgcd(b,a%b);
int temp=x;//x2
x=y;//x1=y2
y=temp-(a/b)*y;//y1=x2-(a/b)*y2;
}
}
2.6.2 有关素数的基础算法
所谓素数,是指恰好有两个约数的n的整数(1和他本身),因为n的约束都不超过n,所以只要检查2~n-1的所有整数是否整除n就能判定n是不是素数。在此,如果d时n的约束,那么n/d也是n的约束。由n=d x n/d可以知道min(d,n/d)<=根号n。
例题:区间内素数的个数
区间[a,b)指的是所有满足a<=x<b的整数所构成的集合。
b以内的合数的最小质因数一定不超过根号b。如果根号b以内的素数表的shan话,就可以把埃氏筛法运用到[a,b)上了,也就是说先分别做好[2,根号b)的表和[a,b]的表,然后从[2,根号b)的表中筛得素数得同时,也将其倍数从[a,b)得表中划去,最后只剩下区间[a,b)内的素数了。
typedef long long ll;
bool is_prime[maxl];
bool is_prime_b[maxb];
void segement_sieve(ll a,ll b){
fill(is_prime,is_prime+(ll)sqrt(b)+5,1);
fill(is_prime_b,is_prime_b+5+b,1);
for(int i=2;(ll)i*i<=b;i++){
if(is_prime[i]){
for(int j=2*i;(ll)j*j<=b;j+=i)is_prime[j]=false;
for(ll j=max(2LL,(a+i-1)/i)*i;j<=b;j+=i)is_prime_b[j-a]=false ;
}
}
}
例题:Crazy Rows (GCJ 2009 Round2 A)
暂且先考虑最后应该把哪一行放在第一行。最后的第一行应该具有00…0或是10…0的形式。可以交换到第1行的行当然也可以交换到第二及以后的行,当有多个满足条件的行是,选择最佳的行对于的最终费用要小。
确定第一行之后,就没有必要再移动它了,于是对于之后的行就可以以同样的思路处理。
在这道题中每行的0和1并不是那么重要,只要知道每行最后一个1所在的位置就1足够了,先将这些位置预先计算好,那么就能降低行交换时的复杂度。复杂度为O(N2)
int N;
int M[maxn][maxn];
int a[maxn];//最后一个1出现的位置
void solve(){
int res=0;
for(int i=0;i<N;i++){
a[i]=-1;//没有1时为-1
for(int j=0;j<N;j++){
if(M[i][j]=='1')a[i]=j;
}
for(int i=0;i<N;i++){
int pos=-1;//要移动到第i行的行
for(int j=i;j<N;j++){
if(a[j]<=i){
pos=i;
break;
}
}
for(int j=pos;j>i;j--){
swap(a[j],a[j-1]);
res++;
}
}
cout << res << endl;
}
例题:2009 Round 1C C
只要不断递归地枚举最初释放地囚犯并计算对应地金币总数,就能求出答案了。
int P,Q,A[MANQ+2];
int dp[MAXQ+1][MAXQ+2];//dp[i][j]表示地是将从A[i]到A[j]号囚犯(不含两端地囚犯)地连续部分里的所有囚犯都是方式,所需地最小金币总数
void solve(){
A[0]=0;
A[Q+1]=P+1;
//为了更方便地处理两端情况,我们把左端当成0号囚犯,右端当作Q+1号囚犯,这样,dp[0][Q+1]就是答案
for(int i=0;i<=Q;i++){
dp[i][i+1]=0;
}
//从短的区间开始填充dp
for(int w=2;w<=Q+1;w++){
for(int i=0;i<W<=Q+1;i++){
int j=i+w,t=INT_MAX;
for(int k=i+1;k<j;k++){
t=min(t,dp[i][k]+dp[k][j]);
}
dp[i][j]=t+A[j]-A[i]-2;
}
}
cout << dp[0][Q+1] << endl;
}