算法 - 算法概论

序言

Fibonacci 数列

多项式时间算法:

var fib_arr = [0, 1];
function fib(n){
  if(n == 0){
    return 0;
  }else if(n == 1){
    return 1;
  }
  for (var i = 0; i < n-2; i++) {
    fib_arr[n+2] = fib_arr[n+1] + fib_arr[n]
  }
  return fib_arr[n]
}
var start = new Date().getTime();
console.log(fib(30));
var end = new Date().getTime();
console.log(‘运行时间:‘+(end-start));
// 运行时间:24

指数时间算法:

function fib(n){
  if(n == 0){
    return 0;
  }else if(n == 1){
    return 1;
  }else{
    return fib(n-1) + fib(n-2);
  }
}
var start = new Date().getTime();
console.log(fib(30));
var end = new Date().getTime();
console.log(‘运行时间:‘+(end-start));
// 运行时间:50

渐进

  • 上界:Ο
  • 下界:Ω
  • 紧确界:Θ

数字的算法

基础算数

基数和对数

  • b 为基数的 k 位数字最大为 b^k-1
  • b 为基数表示 N 需要 ?log(b)(N+1)? 位(应该是向上取整)

乘法(Al Khwarizmi 乘法规则)

类似使用二进制进行乘法

y(每次整除 2) z(结果,由底至顶)
11 13+(13+(13*2)*2)*2
5 13+(13*2)*2
2 13*2
1 13
0 0
// 类似使用二进制进行乘法(当y>=0时可用)
function multiply(x, y){
  if(0 == y){
    return 0;
  }
  var z = multiply(x, Math.floor(y/2))
  // y为偶数,y为奇数加到x上
  return y%2 == 0 ? 2*z : x + 2*z;
}
console.log(multiply(13, 11));

除法

类似使用二进制进行除法

x(每次整除 2) z(结果,由底至顶)
13 [0+1, ((((0+1)*2+1)*2)*2+1)-11]
6 [0, ((0+1)*2+1)*2]
3 [0, (0+1)*2+1]
1 [0, (0+1)]
0 [0, 0]
// 类似使用二进制进行除法(当y>=1时可用,q:quotient,r:remainder)
function divide(x, y){
  if(0 == x){
    return [0, 0];
  }
  var z = divide(Math.floor(x/2), y);
  var q = 2 * z[0];
  var r = 2 * z[1];
  if (x%2 == 1) {
    // 被除数为奇数除二余数为1
    r++;
  }
  if (r >= y) {
    // 余数大于除数,余数变为余数减去除数,商+1
    r -= y;
    q++;
  }
  console.log(q, r);
  return [q, r];
}
console.log(divide(13, 11));

模运算

定义:x=qN+r,且0<=r<N,则 x 模 N 等于 r。

  • x 与 y 模 N 同余 <=> x≡y(mod N) <=> N 整除 (x-y)
  • 模运算准守结合律、分配率、交换律

模的指数运算

x:10, y:6, N:17

y(指数,每次整除 2) z(结果,由底至顶)
6 ((10*((10*1*1)%17)*((10*1*1)%17))%17)*((10*((10*1*1)%17)*((10*1*1)%17))%17)%17
3 (10*((10*1*1)%17)*((10*1*1)%17))%17
1 (10*1*1)%17
0 0
// 求模的指数运算(y>=1)
function modexp(x, y, N){
  if(0 == y){
    return 1;
  }
  var z = modexp(x, Math.floor(y/2), N);
  if (y%2 == 0) {
    return (z * z) % N;
  }else{
    return (x * z * z) % N;
  }
}
console.log(modexp(11, 11, 2));//1
console.log(modexp(10, 11, 2));//0
console.log(modexp(10, 6, 17));//9

Euclid 最大公因数(greatest common divisor)算法

  • Euclid 规则:如果 x,y 是正整数,且有 X>=y,那么 gcd(x, y) = gcd(x mod y, y)
// Euclid求最大公约数方法(a>=b>=0)
function euclid(a, b){
    if(0 == b) return a;
    return euclid(b, a % b);
}
console.log(euclid(11, 11));//11
console.log(euclid(33, 22));//11
console.log(euclid(97, 79));//1
  • Euclid 拓展:用来求gcd(a, b) = d = ax + by的 x 和 y
// Euclid拓展(a>=b>=0)
function extend_euclid(a, b){
    if(0 == b) return [1, 0, a];
    var result = extend_euclid(b, a % b),
      x = result[0],
      y = result[1],
      d = result[2];
    return [y, x-Math.floor(a/b)*y, d];
}
console.log(extend_euclid(11, 11));//[ 0, 1, 11 ]
console.log(extend_euclid(33, 22));//[ 1, -1, 11 ]
console.log(extend_euclid(97, 79));//[ 22, -27, 1 ]

注:a mod b = a - ?a/b?*b

模的除法运算

不确定!!!

例,求 100/27 mod 97

求乘法逆元:
97*22 + 79*-27 = 1
=> 79*-27 = 1 mod 97
=> -79 = 1/27 mod 97

乘法逆元带入:
100/27 mod 97
= 100 * 1/27 mod 97
= 100 * -79 mod 97
= 54

素性测试

  • 费马小定理:p 为素数,任意对于任意 1<=a<p 有a^(p-1) = 1 mod p
// 素数测试算法(对于合数N,a的大多数无法通过测试)
function primality(N){
  var a = Math.floor(2 + Math.random() * (N-3));
  if (modexp(a, N - 1, N) == 1) {
    return true;
  }else{
    return false;
  }
}

// 求模的指数运算(y>=1),前面的算法
function modexp(x, y, N){
  if(0 == y){
    return 1;
  }
  var z = modexp(x, Math.floor(y/2), N);
  if (y%2 == 0) {
    return (z * z) % N;
  }else{
    return (x * z * z) % N;
  }
}
console.log(primality(11027));
console.log(primality(11026));
// 素数测试算法(对于合数N,a的大多数无法通过测试)
function primality(N){
  // 降低出错概率到(2^-100)
  for (var i = 0; i < 100; i++) {
    var a = Math.floor(2 + Math.random() * (N-3));
    if (modexp(a, N - 1, N) != 1) {
      return false;
    }
  }
  return true;
}

// 求模的指数运算(y>=1),前面的算法
function modexp(x, y, N){
  if(0 == y){
    return 1;
  }
  var z = modexp(x, Math.floor(y/2), N);
  if (y%2 == 0) {
    return (z * z) % N;
  }else{
    return (x * z * z) % N;
  }
}
console.log(primality(11027));
console.log(primality(11026));

密码学

一次一密乱码本(one-time pad)和 AES

使用密钥 r 进行异或加密解密

  • 加密:er(x) = x ⊕ r
  • 解密:er(er(x)) =(x ⊕ r) ⊕ r = x ⊕ (r ⊕ r) = x

一次一密乱码本

r 与发送信息长度一样

const r = "101011100";

function encrypt(x){
  return (parseInt(x, 2) ^ parseInt(r, 2)).toString(2);
}

function unencrypt(x){
  return (parseInt(x, 2) ^ parseInt(r, 2)).toString(2);
}

// test
var msg = "111110000";
var encrypted = encrypt(msg);
console.log(`encrypted: ${encrypted}`);
var unencrypted = unencrypt(encrypted);
console.log(`unencrypted: ${unencrypted}`);

AES

  1. r 通常定为 128 位(也有定为 192 位或 256 位的情况)
  2. 定义一种双向映射机制 er
  3. 信息分成片段,每个片段用 er 加密

RAS

生成密钥

  1. 找大整数 p,q(素数测试算法)
  2. 根据 N=pq,求 N(乘法)
  3. 找任意与 (p-1)(q-1) 互素的数 e
  4. 根据 ed≡(1 mod (p-1)(q-1)),求 d(拓展 Euclid 算法)

此时公钥为 (N,e),私钥为 (N,d)

class ras {
    static genrsa() {
        let p, q, N, e, d;
        // 产生两个大素数
        p = this._genpri(4);
        q = this._genpri(4);
        // 求N
        N = p * q;
        // 求e和d
        [e, d] = this._get_eexp_and_inv((p - 1) * (q - 1));
        return {
            // 公钥
            pub_key: {
                N: N,
                e: e
            },
            // 私钥
            pri_key: {
                N: N,
                d: d
            }
        }
    }

    static encrypt(x, N, e) {
        return this._modexp(x, e, N);
    }

    static unencrypt(y, N, d) {
        return this._modexp(y, d, N);
    }

    // 生成素数
    static _genpri(size) {
        while (true) {
            let n = Math.floor((1 << size) - 1 + Math.random() * (1 << size));
            if (this._primality(n)) {
                return n;
            }
        }
    }

    // 获取加密指数和乘法余元(encryption exponent and multiplicative inverse)
    static _get_eexp_and_inv(N) {
        while (true) {
            let e = this._genpri(2);
            let d = this._extend_euclid(N, e)[1];
            // d > 1:乘法余元非负,且与N互素(模的指数运算无法使用负数)
            if (d > 1) {
                return [e, d];
            }
        }
    }

    // 模的指数运算
    static _modexp(x, y, N) {
        if (0 == y) {
            return 1;
        }
        // static模式下arguments.callee失效
        var z = this._modexp(x, Math.floor(y / 2), N);
        if (y % 2 == 0) {
            return (z * z) % N;
        } else {
            return (x * z * z) % N;
        }
    }

    // 素数测试算法(对于合数N,a的大多数无法通过测试)
    static _primality(N) {
        // 降低出错概率到(2^-100)
        for (var i = 0; i < 100; i++) {
            var a = Math.floor(2 + Math.random() * (N - 3));

            if (this._modexp(a, N - 1, N) != 1) {
                return false;
            }
        }
        return true;
    }

    // Euclid拓展(a>=b>=0)
    static _extend_euclid(a, b) {
        if (0 == b) return [1, 0, a];
        var result = this._extend_euclid(b, a % b),
            x = result[0],
            y = result[1],
            d = result[2];
        return [y, x - Math.floor(a / b) * y, d];
    }
}

// test
let msg = 123;
let test_res = ras.genrsa();
console.log(`test_res:`, test_res);
let encrypted = ras.encrypt(msg, test_res.pub_key.N, test_res.pub_key.e);
console.log(`encrypted: ${msg} -> ${encrypted}`);
let unencrypted = ras.unencrypt(encrypted, test_res.pri_key.N, test_res.pri_key.d);
console.log(`unencrypted: ${encrypted} -> ${unencrypted}`);

加密解密

  1. 加密:y=x^e mod N
  2. 解密:x=y^d mod N

破解?

  • 方法 1:尝试所有可能的 x 判断x^e = y mod N是否成立
  • 方法 2:对 N 因式分解,得到 p 和 q,进而计算出 d

散列表

散列表大小设为素数冲突概率小!

例:IP 地址的散列

// 未做解决冲突
function hash_IP(num) {
    var a = [],
        ip_table = new Array(num);
    for (let i = 0; i < 4; i++) {
        a[i] = Math.floor(Math.random() * num);
    }
    return function (IP) {
        let d, sum = 0;
        d = IP.split(‘.‘);
        for (let i = 0; i < 4; i++) {
            sum += +d[i] * a[i];
        }
        ip_table[Math.floor(sum % num)] = {
            ip: IP,
            data: IP
        }
        return ip_table;
    }
}
var hi = hash_IP(100)
console.log(hi(‘127.0.0.1‘));
console.log(hi(‘60.59.44.33‘));
console.log(hi(‘192.110.119.115‘));

分治算法

乘法

递归将乘数分为两段,并将 T(n) = 4T(n/2) + O(n) 的方法优化为

T(n) = 3T(n/2) + O(n) 的方法

效率:
由普通的 O(n^2) 提高到 O(3^log2(n))(约为 O(n^1.59))
PS: O(3^log2(n)) = O(n^log2(3)) 指数对数一顿推就出来了

算法:

/**
 * 分治整数乘法算法
 * @param  {number} x 被乘数,整数,大于等于0
 * @param  {number} y 乘数,整数,大于等于0
 * @returns {number} 结果
 */
export default (x, y) => {
    function multiply(x, y) {
        var xl, xr, yl, yr, p1, p2, p3, n, nl, nr;
        n = Math.max(x.toString(2).length, y.toString(2).length);

        // 一位的时候x*y和x&y一样
        if (n == 1) return x & y;

        nl = Math.ceil(n / 2);
        nr = Math.floor(n / 2);

        xl = x >> nr;
        xr = x - (xl << nr);
        yl = y >> nr;
        yr = y - (yl << nr);
        p1 = multiply(xl, yl);
        p2 = multiply(xr, yr);
        p3 = multiply(xl + xr, yl + yr);

        /**
         * 左移的位数要在纸上写几遍把握一下(书上p1左移n位当n为偶数才能成立)
         * 算数运算符优先级大于位移运算符
         * 如:110 * 111 = ((11 << 1) + 0) * ((11 << 1) + 1)
         *  = [(11 * 11 << 2)] + [11 * 1 + 11 * 0 << 1]                 + [0 * 1]
         *  = [p1 << 2]        + [((11 + 0) * (11 + 1) - p1 - p2) << 1] + [p2]
         *  = [p1 << 2]        + [(p3 - p1 - p2) << 1]                  + [p2]
         * 这个式子中n为3,nr为Math.floor(3 / 2)等于1
         */
        return (p1 << (nr << 1)) + ((p3 - p1 - p2) << nr) + p2;
    }

    return multiply(x, y);
}

注:书中最后左移的位数只有偶数位才正确

主定理

T(n) = aT(n/b) + O(n^d)

  • d > logb(a) : T(n) = O(n^d)
  • d = logb(a) : T(n) = O((n^d)*log(n))
  • d <logb(a) : T(n) = O(n^(logb(a)))

递归树宽 a^logb(n),高 logb(n)

合并排序

T(n) = 2T(n/2) + O(n) = O(nlogn)

递归:

test_arr = [10, -1, 2, 3, 9, 10 ,9];
function mergesort(arr){
  if (arr.length > 1) {
    // 将两个排序好的数组进行连接
    return merge(mergesort(arr.slice(0, Math.floor(arr.length/2))),
      mergesort(arr.slice(Math.floor(arr.length/2), arr.length)));
  }else{
    return arr;
  }
}
function merge(arr1, arr2){
  if(arr1.length == 0) return arr2;
  if(arr2.length == 0) return arr1;
  if (arr1[0] > arr2[0]) {
    return [arr1[0]].concat(merge(arr1.slice(1, arr1.length), arr2));
  }else{
    return [arr2[0]].concat(merge(arr2.slice(1, arr2.length), arr1));
  }
}
console.log(mergesort(test_arr));

迭代:

test_arr = [10, -1, 2, 3, 9, 10 ,9];
function interative_mergesort(arr){
  var temp_arr = test_arr.map((d)=>{
    return [d];
  });
  while(temp_arr.length > 1){
    temp_arr.push(merge(temp_arr.pop(), temp_arr.pop()))
  }
  return temp_arr[0];
}
function merge(arr1, arr2){
  if(arr1.length == 0) return arr2;
  if(arr2.length == 0) return arr1;
  if (arr1[0] > arr2[0]) {
    return [arr1[0]].concat(merge(arr1.slice(1, arr1.length), arr2));
  }else{
    return [arr2[0]].concat(merge(arr2.slice(1, arr1.length), arr1));
  }
}
console.log(interative_mergesort(test_arr));

寻找中项(选择有序数组的某项)

用快速排序的思想,递归找出 SL(小于 v 的数)、SV(等于 v 的数)、 SR(大于 v 的数)缩小范围

代码

效率:
最好:T(n) = T(n/2) + O(n) = O(n)
最坏:T(n) = T(n - 1) + O(n) = O(n^2)

PS:快速排序是超大文件排序算法的基础

矩阵乘法

使用T(n) = 7T(n/2) + O(n^2)的分治算法可以从传统的 O(n^3) 优化到 O(n^log2(7))

留坑以后写

快速傅里叶变换(FFT)

预备知识

一个 d 次多项式被其在任意 d+1 个不同点出的取值所唯一确定

d+1 个不同点处的取值可以列出 d+1 个方程,解出系数 a0, a1, ..., ad(共 d+1 个)

多项式A(x)=a0*(x^0) + a1*(x^1) + ... + ad*(x^d)的两种表示

  1. 系数:a0, a1, ..., ad
  2. 值:A(x0), A(x1), ..., A(xd)

系数与值的关系

系数 -> 计算 -> 值
值 -> 插值 -> 系数

问题转化

计算 A(x)*B(x) = C(x) 的问题转化成计算 A(x) 和 B(x) 在 2d+1(d 为多项式次数)个不同点处的取值的乘积,然后对这些乘积插值获取 C(x) 的系数

从直接计算和插值开始

下面有一个直接进行计算和插值求多项式乘积的算法,可以看出问题有:

  1. 计算:计算增广矩阵的时间复杂度是 O(n^2)(这里和直接乘起来一样复杂了)
  2. 插值:求非齐次线性方程组的时间复杂度是 O(n^3)(反而更加复杂了)

原本以为用 0,2,4,8... 这些点是不可以简化计算和插值,结果发现想多了简化不了,就用了 0,1,2,3... 这种取值。

FFT 正是巧妙地选择了计算的点使算法效率提高。

/**
 * 使用未经优化的计算和插值方法,计算多项式A(x)*B(x)的系数
 * @param  {Array<number>} Ax 多项式A(x)的系数
 * @param  {Array<number>} Bx 多项式B(x)的系数
 * @returns {Array<number>} 多项式A(x)*B(x)的系数
 */
export default (Ax, Bx) => {
    let d,
        am = [], //增广矩阵 augmented matrix
        Cx = [];

    if (Ax.length > Bx.length) {
        d = Ax.length - 1;
        Bx[Ax.length - 1] = 0;
        while (Bx.push(0) != Ax.length);
    } else if (Bx.length > Ax.length) {
        d = Bx.length - 1;
        while (Ax.push(0) != Bx.length);
    } else {
        d = Ax.length - 1;
    }

    // 计算增广矩阵(O(n^2))
    for (let i = 0; i < 2 * d + 1; i++) { // 循环2*d + 1次
        let x = 1,
            Av = 0,
            Bv = 0;
        am[i] = [];
        for (var j = 0; j < 2 * d + 1; j++) { // 循环2*d + 1次
            if (j < d + 1) {
                Av += Ax[j] * x;
                Bv += Bx[j] * x;
            }
            am[i][j] = x;
            x *= i;
        }

        am[i][j] = Av * Bv;
    }

    // 之后就是用增广矩阵求非齐次线性方程组了(O(n^3),比直接乘都复杂了)

    // 1.三角阵
    for (let i = 0; i < 2 * d; i++) {
        for (let j = i + 1; j < 2 * d + 1; j++) {
            let ratio = am[j][i] / am[i][i]
            am[j] = am[j].map((v, index) => {
                return v - ratio * am[i][index]
            })
        }
    }

    // 2.对角阵
    for (let i = 2 * d; i > 0; i--) {
        for (let j = i - 1; j > -1; j--) {
            let ratio = am[j][i] / am[i][i]
            am[j] = am[j].map((v, index) => {
                return v - ratio * am[i][index]
            })
        }
    }

    // 3.单位阵(思想)
    for (let i = 0; i < 2 * d + 1; i++) {
        Cx[i] = Math.round(am[i][2 * d + 1] / am[i][i]);
    }

    return Cx;
}

开始 FFT

计算:点的选取

多项式A(x)可以变为Ae(x^2) + x * Ao(x^2)(也就是A(x) = Ae(x^2) + x * Ao(x^2)),例如:

A(x) = 1*x^0 + 2*x^1 + 3*x^2 + 4*x^3 = (1*x^0 + 3*x^2) + x * (2*x^0 + 4*x^2)

这时如果选取的点为正负的数对进行计算,就可以简化将近一半的计算量,例如:

x1取1,x2取-1

这时计算:
A(x1) = Ae(x1^2) + x1 * Ao(x1^2)
A(x2) = Ae(x2^2) + x2 * Ao(x2^2)

只需计算:P1 = Ae(1) 和 P2 = 1 * Ao(1)
A(x1) = P1 + P2
A(x2) = P1 - P2

这里的 Ae(x) 和 Ao(x) 也是可以再分的,这样就可以使用分治了,但选取的正负的数对的平方还得是正负的数对这样才可以简化运算,这样的数存在么?当然存在那就是复数(这里得补点课),例如:

取4个点:
x1取1,x2取-1,x3取i,x4取-i
平方   1             -1
平方           1

取8个点:
取复数 1+0, -1+0, 0+i, 0-i, √2+√2i, -√2-√2i, √2-√2i, -√2+√2i
平方       1+0      -1+0         0+i              0-i
平方             1                         -1
平方                           1

以此类推,取16个点,取32个点。。。

插值:用计算的算法就可解决

用计算的算法就可解决插值问题,那么原理是什么呢?计算相当于n 阶的方阵 M * n 个系数,插值相当于n 阶的方阵 M 的逆矩阵 * n 个计算出来的值,且这个方阵满足M(w)^-1 = 1/n * M(w^-1),证明如下:

证明:略

图的分解

深度优先搜索(Depth-First-Search)

对图进行深度优先搜索得到森林

代码

Algorithms-JS/dfs.js at master · 1010543618/Algorithms-JS

有向图分解为强连通部件

步骤

  1. 找到出度为 0 的强连通部件(汇点强连通部件)
  2. 删去该强连通部件
  3. 重复执行 1 和 2,直到没有顶点

如何找到汇点强连通部件

因为对图 G 进行深度优先搜索得到的 post 最大的点(最后一个出栈的点)一定位于源点强连通部件中,

PS:注意对图进行深度优先搜索得到森林,而不是仅遍历可以达到的节点。这样从非源点强连通部件的节点开始遍历其 post 的值会加到下一个开始遍历的强连通部件中。例如:

(强连通部件C)--->---(强连通部件C‘)

若是从C开始遍历post最大的点(最后一个出栈的点)是C中的点

若是从C‘开始遍历,post最大的点(最后一个出栈的点)还是C中的点

这样将图 G 翻转得到 GR,对 GR 进行深度优先搜索得到的 post 最大的点(最后一个出栈的点)一定位于汇点强连通部件中。

算法

  1. 将图 G 翻转得到 GR
  2. 对 GR 进行深度优先搜索,按出栈顺序(post 的值)降序排列顶点
  3. 依次从排序后的顶点开始进行深度优先搜索,找到汇点强连通部件
  4. 删除该汇点强连通部件
  5. 重复执行 3 和 4,直到没有顶点

代码

Algorithms-JS/get_strongly_connected_component.js at master · 1010543618/Algorithms-JS

图中的路径

广度优先搜索(Breadth-First Search)

广度优先搜索关注某一顶点所有能达到的顶点的搜索深度。

贪心算法

最小生成树

Huffman 编码

Hron 公式

SAT 的一种特殊形式,可在线性时间求解。

Horn-satisfiability - Wikipedia

动态规划

最长递增子序列

编辑距离

背包问题

多副本背包问题

背包容量递增,对每个物品判断加入后是否最优。(要保存先前每个背包容量最优状态下的价值

单副本背包问题

物品数递增,判断加入当前物品是否最优。(要保存先前每个物品数最优状态下的重量价值

矩阵链式相乘

注意:求解方向如下表

j\i 1 2 3 4 5
1 1 最先求解
2 2 其次求解 1 最先求解
3 3 2 其次求解 1 最先求解
4 4 3 2 其次求解 1 最先求解
5 5 最后求解 4 3 2 其次求解 1 最先求解

最短路径

最短可靠路径

Dijkstra 中保存经过节点个数这一信息。

所有节点间的最短路径

Floyd-Warshall 算法:

  1. 初始两点间路径为两点间的距离dist(i,j,0) = l(i,j)
  2. 依次加入顶点 k,计算加入顶点后的最短路径dist(i,j,k) = min(dist(i,k,k-1)+dist(k,j,k-1),dist(i,j,k-1))

旅行商问题(TSP)

子问题:由起点出发经过城市集合S 终点为j经过S集合中每个顶点各一次的最短路径C(S,j)

树中的独立集

树的独立集可以利用线性规划在线性时间求解。

子问题:子树的最大独立规模。

线性规划(LPs)与归约

单纯形法

线性规划都可以用单纯形法求解

步骤:从一个顶点开始,不断寻找目标函数值较高的顶点,当找不到目标函数值更高的顶点,则当前顶点为最优解。

归约(reduce)

将复杂的问题转化为同等具有代表性的问题

如:求解 dag 的最长路径可以归约到求解最短路径。(取倒数求最短路径即可得到最长路径)

网络流

最大流

步骤:

  1. 寻找一条还有剩余流量的路径;
  2. 找到该路径所有边的剩余流量的最大值m
  3. 对路径所有边正向减去m,反向加上m
  4. 重复 1、2、3,直到找不到有剩余流量的路径。

最小分割

得到最大流后,搜索起点能达到的顶点集合L,令R=V-L,集合R和集合L即为最小分割

二部图的匹配

可以被归约到最大流问题。(通过添加起点,终点并对所有边容量都设置为 1)

对偶

LP 的最大问题都有对偶的最小问题。

对偶定理:如果一个线性规划的最优目标函数值有界,则其对偶的最优目标函数值也有界,并且二者相等

零和博弈(游戏)

单纯形算法

  • 顶点:由 n 个不等式定义。
  • 邻居:两个顶点对应的所有不等式有 n-1 个相同

算法

  1. 坐标变换将顶点移动到原点
  2. 判断是否最优
    • 非最优:原点移动到邻居 u,并使 u 成为新的坐标原点
    • 最优:返回结果
  3. 重复 2 直到找到最优

电路值

电路值问题可以归约到线性规划。

如果算法的复杂度是多项式的,则它就能被视为一个包含了多项式数量的计算机电路拷贝的布尔电路。

NP - 完全问题

搜索问题

搜索问题是由一个算法C描述的,以问题实例I和一个可能的解S为输入,运行在关于|I|的多项式时间内。我们说SI的一个解,当且仅当C(I,S)=true

根据摩尔定律,时间复杂度为指数的算法的能力将以多项式速度提高,时间复杂度为多项式的算法的能力将以指数熟读提高。

可满足性问题(STA)

Horn 公式和 2SAT 可在线性时间求得。

旅行商问题(TSP)

给定一个实例,寻找一个总费用在预算内的可行路线。

Euler 问题和 Rudrata 问题

Euler 问题(七桥问题):给定一个图,求一条恰好包含每条边一次的路径。

Rudrata 问题(马踏棋盘问题):给定一个图,求一条经过每个顶点恰好一次的环路。

Euler 问题和 Rudrata 问题复杂度存在巨大差异的原因在于一个是关注的是边,另一个关注的是顶点。

最小分割和平衡分割

最小分割:给定图和整数 g,求最多包含 g 条边的分割。

平衡分割:给定图和整数 g,将所有顶点分割为ST,使得|S|>=3|T|>=3,且ST之间最多包含 g 条边。

3D 匹配

二部图匹配的推广。

独立集、顶点覆盖和团

独立集:给定图和整数 g,求图中 g 个相互独立的顶点。

顶点覆盖:给定图和整数 g,求图中 g 个能覆盖到每条边的顶点。

团:给定图和整数 g,求图中 g 个两两相连的顶点。

最长路径问题

给定图和整数 g,s 到 t 总权重最少为 g 的路径(不包含重复的顶点)。

背包问题与子集合

子集合:设背包问题的物品价值都等于重量,求最大收益(使带走物品总重量等于负重)。

NP - 完全问题

困难的问题(NP - 完全) 容易的问题(P)
3SAT 2SAT、HORN SAT
旅行商问题 最小生成树
3D 匹配 二部图匹配
背包问题 一元数背包问题
独立集 树的独立集
整数线性规划 线性规划
Rudrata 路径 Euler 路径
平衡分割 最小分割

任意两个困难的问题可以相互归约。

P = NP?

P:多项式的时间(polynomial time)

NP:不确定多项式的时间(nondeterministic polynomial time)

另一种说法:是否存在不能在多项式时间求解的问题?

归约

称一个搜索问题是 NP - 完全的,是指其他所有搜索问题都可以归约到它。

A 归约为 B,B 的难度大于 A。问题的难度沿着归约方向移动,NP - 完全问题是最难的(其他所有搜索问题都可以归约到它)。

NP 问题:

  • ZOE:给定 0-1 值得 M*N 矩阵 A,求一个 0-1 列向量 x 使得Ax = 1,1 表示所有分量都为 1 的行向量(书中貌似有误)。
  • ILP:求满足 Ax <= b 的整数向量 x。
  • 含有成对边的 Rudrata 环路:给定一个图G=(V, E) 和一个集合C(C 属于 E*E),求经过所有顶点一次(Rudrata 环路的要求),且对C中的每个边对(e,e‘)只能用其一(用了 e 就不能用 e‘)(成对边的要求)的环路。
所有NP问题
    SAT(将所有NP问题转化为电路)
        3SAT(给SAT增加变量)
            独立集(将3SAT变量作为顶点,条件作为边)
                顶点覆盖(|V|-g 个节点的顶点覆盖)
                团问题(图G的补集的团)
        3D匹配(使用四个三元组模拟布尔变量)
            ZOE(将3D匹配的图转化为邻接矩阵求解)
                子集合(将ZOE的列视为二进制整数)
                ILP(将ZOE的约束方程重写为两个不等式,对每个变量增加新约束)
                Rudrata环路(ZOE -> 含有成对边的Rudrata环路 -> Rudrata环路)
                    TSP(Rudrata环路的实例视为”城市间距离为1,预算等于顶点个数“的TSP的解)
                

原文地址:https://www.cnblogs.com/jffun-blog/p/10217677.html

时间: 2024-10-29 10:28:20

算法 - 算法概论的相关文章

[数据结构和算法]算法基本概念

算法基本概念: 算法:用来对数据的操作作描述,是对问题求解的步骤的描述.是一系列将输入转为输出的计算步骤 算法复杂度:分为时间复杂度和空间复杂度时间复杂度:算法中所有语句的频度之和用T(n)表示,记为T(n) = O(n) 常见时间复杂度递增次序:常数 O(1) , 对数阶O(log2^n) , 线性阶O(n) , 线形对数阶O(nlog2^n),平方阶O(n^2),立方阶O(n^3),指数阶O(2^n),O(n!),O(n^n)当n值增大,算法时间复杂度即变大,执行效率变低 最坏时间复杂度:最

剪枝算法(算法优化)

一:剪枝策略的寻找的方法 1)微观方法:从问题本身出发,发现剪枝条件 2)宏观方法:从整体出发,发现剪枝条件. 3)注意提高效率,这是关键,最重要的. 总之,剪枝策略,属于算法优化范畴:通常应用在DFS 和 BFS 搜索算法中:剪枝策略就是寻找过滤条件,提前减少不必要的搜索路径. 二:剪枝算法(算法优化) 1.简介 在搜索算法中优化中,剪枝,就是通过某种判断,避免一些不必要的遍历过程,形象的说,就是剪去了搜索树中的某些"枝条",故称剪枝.应用剪枝优化的核心问题是设计剪枝判断方法,即确定

c++ 提高4 map容器 共性机制 使用时机 比较| STL算法 算法基础仿函数 谓词 函数适配器 遍历算法

[本文谢绝转载] <大纲> STL 容器 map 容器的4中初始化 遍历 map容器 元素的删除观测map.insert返回值,方法123,已存在就报错,初始化方法4会覆盖 map的查找,异常处理 map容器的range返回两个迭代器 multimap案例,按照部门_增删员工信息 容器共性机制 把对象放到容器中,会自动执行拷贝构造函数 各个容器的使用时机 vector与deque的比较: 算法 算法基础 函数对象(仿函数) 函数对象 与普通函数的区别:--  相同之处 函数对象 与普通函数的区

C数据结构与算法-算法复杂度

算法复杂度分为时间复杂度T(n)和空间复杂度F(n) 时间复杂度:也就是执行算法程序所需的时间,与硬件的速度.编程语言的级别.编译器的优化.数据的规模.执行的频度有关,前三个有很大的不确定性,所以衡量指标只要是后两者即算法的时间复杂度是数据规模n的函数.T(n)=O(F(n)),其中O表示同阶,即当n趋近无穷大是T(n)与F(n)的比值是个不为0的常数,也就是渐进时间复杂度.按照时间复杂度量级递增顺序为:常数阶O(1).对数阶O(log2n).线性阶O(n).线性对数阶O(nlog2n).平方阶

腾讯t4架构推荐:一份程序员必备的结构算法算法之道!

引言 "语言只是工具,算法才是程序的灵魂."这句话相信每一个程序员都听过无数次.然而在实际的工作中,一个产品从开发到上线,似乎哪一步都用不到数据结构与算法.于是很多程序员都有这样一种错觉:就算我不懂算法,只要语言写得溜.开发框架用得熟练.封装好的各种接口.库调用得熟练,照样能实现老板“天马行空”(傻逼)的需求,照样可以混到高薪.算法的学习,别人帮不了你多少,主要还是的靠自己悟,靠自己花时间去理解,师傅领进门,修行在个人,算法讨论,思路分享,去牛客网看看,各路大神,各种解决思路,你一定会

算法算法算法

花了将近一个半月时间,踏踏实实的过了一边数据结构和算法.之前一直断断续续看过好几次,但没有一次完整的看完.这次把这个坑终于填上了. 虽然说,在 Python 大行其道的今天,算法都被隐藏了,像我在现在的公司,从来都不会接触算法,Python 的简单明了,开发效率,足以顺利漂亮的完成项目.但是一接触 Deep Learning 相关的资料,首当其冲的算法问题就横亘在面前. 生活依然在继续,抓紧时间,好好努力.

0-1背包的动态规划算法,部分背包的贪心算法和DP算法------算法导论

一.问题描述 0-1背包问题,部分背包问题.分别实现0-1背包的DP算法,部分背包的贪心算法和DP算法. 二.算法原理 (1)0-1背包的DP算法 0-1背包问题:有n件物品和一个容量为W的背包.第i件物品的重量是w[i],价值是v[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大.其中每种物品只有一件,可以选择放或者不放. 最优子结构性质:对于0-1问题,考虑重量至多W的最值钱的一包东西.如果去掉其中一个物品j,余下的必是除j以外的n-1件物品中,可以带走的重量

回溯算法——算法总结(四)

回溯算法也叫试探法,它是一种系统地搜索问题的解的方法.回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试.用回溯算法解决这个问题的一般步骤为: 1.定义一个解空间.它包括问题的解. 2.利用适于搜索的方法组织解空间. 3.利用深度优先法搜索解空间. 4.利用限界函数避免移动到不可能产生解的子空间. 问题的解空间一般是在搜索问题的解的过程中动态产生的.这是回溯算法的一个重要特性. 经典样例(N后问题): 要求在一个n*n格的棋盘上放置n个皇后,使得她们彼此不受攻击,即使得

算法——算法时间复杂度的计算和大O阶的推导

在算法分析中,我们将语句总的执行次数记为T(n)进而分析T(n)随n的变化情况确认T(n)的数量级.一般情况下,T(n)随n增大变化最缓慢的算法为最优算法. 根据定义,T(n)的求法是很简单的,也就是简单的数数.举个例子: int i; for(i=0;i<n;i++); 这里int执行一次,for循环里的语句执行n次,所以T(n)=n+1;但是当n变大时,这个常数就显得无足轻重了,所以它的算法复杂度为O(n). 同样的,对于下面的代码: int i,j; for(i=0;i<n;i++) f