您的位置:首页 > 其它

算法核心思想总结及模板(不定期更新)

2017-02-18 11:52 267 查看
写这篇博客的目的:

加强对算法的理解和概括

备忘

快排(divide-and-conquer)

核心思想:

  每一次划分将小于或等于pivot element(下面第二张图中 x)的元素依次放在前面,则大于x的元素就自动到后面一部分了,然后再把x放到两部分中间(此时x已排好序,归位),然后对左右两部分递归调用。





[align=right]Update:2017/2/18 [/align]

筛法求素数

核心思想:

  把合数筛掉。

  任何一个合数都可以分解成几个质数相乘。

  换句话说,则任何一个合数都有质因子,且 合数=质因子∗x,x是一个大于或等于2的数。

int vis
;

void prime(int n)
{
mst(vis,0);
for(int i=2;i<=n;i++)
{
if(!vis[i])//如果是素数
{
for(int j=2*i;j<=n;j+=i)//有重复判定,比如6=2*3=3*2,改成从j=i*i开始即可避免,但要小心i*i爆int
vis[j]=1;
}
}
}


复杂度:接近线性。

[align=right]Update:2017/2/24 [/align]

动态规划

用于解决具有如下性质的问题:

最优子结构 (Optimal substructure)

  

  原问题的最优解包含子问题的最优解。

     

子问题重叠 (Overlapping subproblems)

  subproblems的重复子问题为subsubproblem,动态规划重复子问题只计算一遍。

子问题不相关 (Independent subproblems)

  一个子问题的解与同层其他子问题不相关,这里的不相关指的是不受其约束(算法导论:Two subproblems of the same problem are independent if they do not share resources)。

实现的两种方式:

自顶向下+记忆(top-down memoized algorithm)

  有额外的递归调用函数的开销,但只会计算在求原问题最优解的过程中必须算的子问题。 

自底向上(bottom-up algorithm)

  没有额外的递归调用函数的开销,但所有子问题都会计算一遍。

子问题图(subproblem graph): 

包含的信息:

  在求原问题最优解的过程中涉及到的子问题(顶点)及子问题之间的依赖关系(边)。

顶点和边:

顶点,对应子问题。

边,对应求解子问题时面临的多种选择,每一种选择又会产生新的子问题(也就是边连接到的孩子顶点)。

通常情况下,动态规划算法的运行时间与顶点和边的数量呈线性关系。

[align=right]Update:2017/2/26 [/align]

贪心算法

用于解决具有如下性质的问题:

贪心选择性质(Greedy-choice property)

  全局最优解由局部最优(贪心)的选择组成,换句话说,在做选择的时候, 选择在当前问题中看起来最优的解,而不去考虑子问题的结果如何。(算法导论:The first key ingredient is the greedy-choice property: we can assemble a globally optimal solution by making locally optimal (greedy) choices. In other words, when we are considering which choice to make, we make the choice that looks best in the current problem, without considering results from subproblems.)

  

  证明某种贪心方式的贪心选择性质不成立:一般用举反例的方法。

  当某种贪心方式的贪心选择性质不成立时,可以考虑换一种贪心方式或考虑用动态规划。

最优子结构 (Optimal substructure)

  

  原问题的最优解包含子问题的最优解。

即:原问题最优解=贪心选择+子问题最优解

     

[align=right]Update:2017/3/11 [/align]

并查集

核心思想:

  找到每个点x所在连通分量的祖先节点(根结点)r,然后将x的父节点直接更新为r,即f[x]=r。

int f
;//父节点

int Find(int x)
{
return x==f[x]?x:f[x]=Find(f[x]);
}
void Union(int x,int y)
{
f[Find(x)]=Find(y);//简写,不管两者的父节点是否一样,都赋值一遍。
}

int main()
{

for(int i=1;i<=n;i++)
f[i]=i; //初始化,每个点都是自己的根结点
for(int i=1;i<=m;i++)
Union(edge[i].u,edge[i].v);

return 0;
}


[align=right]Update:2017/3/10 [/align]

欧几里得算法(最大公约数gcd)

int gcd(int a,int b)
{
return (!b)?a:gcd(b,a%b);
}


[align=right]Update:2017/3/19 [/align]

扩展欧几里得算法

核心思想:

  利用欧几里得算法过程中gcd(a,b)=gcd(b,a mod b)来求解ax+by=gcd(a,b)的一个整数解(包括负数)。

初始条件:

当b==0时,ax+by=gcd(a,b)的一个整数解为(x,y)=(1,0)

作出假设:

假设已知上层的解x’, y’,即bx’+(a mod b)y’=d’①的解

推理:

现要求本层ax+by=d ②的解

其中d=gcd(a,b)=d’=gcd(b,a mod b) ③

又对任意整数a和任意正整数n,有



将③④代入①,得



⑤和②进行比较,即可得本层的解


上面的论证有点像数学归纳法和递推。

上述过程其实就是将①式整理成②式的形式,从而得到②式的一个解,因为从始至终都只是1个方程2个未知数,所以最后结果只能得到由初始解(1,0)经过层层映射得到的原方程的一个整数解,若将初始解更换成其他的合理解时,结果也将跟着发生变化。

伪代码:



int ex_gcd(int a,int b,int &x,int &y)
{
if(b==0)
{
x=1; y=0;
return a;
}
int d=ex_gcd(b,a%b,x,y);
int xx=x;
x=y;
y=xx-a/b*y;
return d;
}


延伸:

求解线性方程ax+by=c的一组整数解

c % gcd(a,b) != 0,此时无解(左右两边同时除以gcd(a,b)后,左边为整数,右边不是)。

c % gcd(a,b) == 0,(x’ , y’)=ex_gcd(a,b,x,y),解x=x’ * (c/gcd(a,b)),y=y’ * (c/gcd(a,b))

求解模线性方程ax≡b (mod n)

  同余方程 ax≡b (mod n) (也就是 ax % n = b) 对于未知数 x 有解,当且仅当 gcd(a,n) | b (也就是 b % (gcd(a,n))==0 )。且方程有解时,方程有 gcd(a,n) 个解。

 求解方程 ax≡b (mod n) ,也就是 ax % n = b,相当于求解方程 ax+ ny= b(x, y为整数)。

求解乘法逆元

  

  当a与n互素时,a在模n下的乘法逆元才存在,并且唯一。

 对同余方程 ax≡ 1(mod n ),即ax+ny=1, 当gcd(a,n)= 1时,原方程才有解,且解唯一,解x即为a在模n下的乘法逆元。

 
[align=right]Update:2017/3/31 [/align]

  

最短路

floyd算法

  floyd算法实质是动态规划,状态转移方程dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j],dp[k][i][j]为从i到j可以经过1~k这些点的情况下的最短路径,则dp[0][i][j]即为从i到j不经过任何点的最短距离。有点类似背包问题。因为状态转移方程左边全是k,右边全是k-1,所以可以升序枚举k从而降成两维。(下面的代码是从0开始编号的,因为k的那一维已经降掉了,k在代码中只是起单纯的计数作用,所以从0开始对顶点编号没有什么影响)

  

//初始化
for(int i=0;i<n;i++)
{
for(int j=0;j<n;j++)
{
if(i==j) dp[i][j]=0;
else dp[i][j]=INF;
}
}

//...输入

//floyd
for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);


[align=right]Update:2017/3/20 [/align]

快速幂

核心思想:

  将幂用2进制展开,然后按权重累乘。

  比如说25=2101=2100∗21,在下面的代码中,b每次向右移一位,则其末尾的那位权重上升成原来的两倍,而当b&1=1时,res乘的是a,所以a要及时地正确地变化,设当前a=2b′,则当b右移一位后a′=22b′=a2,这就是为什么b每次右移一位,a都要平方一下的原因。

  

long long quick_pow (long long a, long long b, long long p) //a的b次幂对p取余
{
long long res = 1;
while(b)
{
if(b&1) res = (res * a) % p;
a = (a*a) % p;
b >>= 1;
}
return res;
}


[align=right]Update:2017/4/4 [/align]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  算法 总结