《算法导论》摘记/经典算法题集锦

分治策略

Diogenes教授有n个被认为是完全相同的VLSI芯片,原则上它们是可以互相测试的。教授的测试装置一次可测二片,当该装置中放有两片芯片时,每一片就对另一片作测试并报告其好坏。一个好的芯片总是能够报告另一片的好坏,但一个坏的芯片的结果是不可靠的。这样,每次测试的四种可能结果如下:

    A芯片报告         B芯片报告     结论

    B是好的          A是好的      都是好的,或都是坏的

    B是好的          A是坏的      至少一片是坏的

    B是坏的          A是好的      至少一片是坏的

    B是坏的          A是坏的      至少一片是坏的

Q:

1.证明如果超过n/2个芯片是坏的,使用任何基于这种逐对检测操作的策略,教授都不能确定哪些芯片是好的。

2.如果超过n/2个芯片是好的,如何O(n)找出所有好的芯片?

解:对于问题2,只需找出1个好的芯片,就能确定所有的芯片的好坏。

粗暴的方法是拿一个芯片与其他所有芯片配对,因为好的比坏的多,就可以根据多的确定该芯片的好坏,直到找到一个好芯片为止。复杂度是O(n2)的。同时也说明了Q1(否则无法确定好坏)

O(n)怎么做?递归缩小问题规模。

假设有偶数个芯片。两两配对,有a对检测结果为都是好的,b对检测结果为一好一坏,c对检测结果为都是坏的。

取a对中每对的任意一个芯片,组成a个芯片的子问题。易证明该子问题依然满足好的芯片数量多于坏的芯片。(因为b+c对中坏的 >= 好的)

 

如果有奇数个芯片。两两配对,有a对检测结果为都是好的,b对检测结果为一好一坏,c对检测结果为都是坏的。还有一个零头。

1.如果零头是坏的,则a对中好的芯片 > 坏的芯片,用a对去检测零头,发现有一半以上的检测结果是坏的,反言之 小于一半的检测结果是好的。

2.如果零头是好的,则a对中好的芯片 >= 坏的芯片,用a对去检测零头,发现大于等于一半的检测结果是好的。

根据结果判断零头是好是坏。如果是1,把零头弃掉;如果是2,把零头加进来,这样该子问题依然满足好的芯片数量多于坏的芯片。

 
如此递归缩小问题规模,每次规模至少减半。

找出一个好零件的时间T1(n) = T1(n/2)+O(n) = O(n),
找出其他好零件的时间T2(n) = n−1 = O(n),
所以总的时间T(n)=O(n)。
View Code
 
 
概率分析和随机算法
 
1.生日悖论
关于如何卡字符串hash
概率论是一个很神奇的东西。。。
假设一年有n天,有k个人。
生日相同的两人对的期望是k(k-1)/(2n), 也就是说O(sqrt(n))的人,生日相同的两人对的期望数是1; O(sqrt(n))的人至少有一对生日相同的概率大于1/2。(参见算导)
意思就是n个桶,随机丢sqrt(n)个数据,hash冲突的概率极大!
故一般的hash易被卡。
如果mod 2^64,可人工构造数据卡hash;mod 1e9左右的,可随机构造数据卡hash。
而双hash不易被卡。
以下是一个随机生成字符串的程序。当有两个长度为l的不同字符串hash值相同时输出并退出。
#define NDEBUG
#include <bits/stdc++.h>
#define ll unsigned long long
#define st first
#define nd second
#define pii pair<int, int>
#define pil pair<int, ll>
#define pli pair<ll, int>
#define pll pair<ll, ll>
#define pw(x) ((1LL)<<(x))
#define lson l, m, rt<<1
#define rson m+1, r, rt<<1|1

using namespace std;
/***********/
template <class T>
bool scan (T &ret) {
    char c;
    int sgn;
    if (c = getchar(), c == EOF) return 0; //EOF
    while (c != '-' && (c < '0' || c > '9') ) c = getchar();
    sgn = (c == '-') ? -1 : 1;
    ret = (c == '-') ? 0 : (c - '0');
    while (c = getchar(), c >= '0' && c <= '9') ret = ret * 10 + (c - '0');
    ret *= sgn;
    return 1;
}
template<typename T>
inline int sgn(T a) {return a>0?1:(a<0?-1:0);}
template <class T1, class T2>
bool gmax(T1 &a, const T2 &b) { return a < b? a = b, 1:0;}
template <class T1, class T2>
bool gmin(T1 &a, const T2 &b) { return a > b? a = b, 1:0;}
const int inf = 0x3f3f3f3f;
const ll INF = 1e18;
/***********/


const ll Mod = 1e9+7, base = 997, l = 20, N = 500000;
char s[N+10];
map<ll, string> ma;
void init(){
    srand(time(0));
    int len = 0;
    while(len < N)
        s[len++] = rand()%26+'a';
}
int main(){
    init();

    ll hash_pow_l = 1;
    for(int i = 1; i <= l; i++)
        hash_pow_l = (hash_pow_l * base) % Mod;

    ll val = 0, val2 = 0;
    for(int i = 0; i < l; i++)
        val = (val * base + s[i] - 'a' + 1) % Mod;
    ma.insert( {val, string(s, s+l)} );

    string t1, t2;
    for(int i = l; i < N; i++){
        val = (val*base+s[i]-'a'+1)%Mod;
        val = (val+Mod - (s[i-l]-'a'+1)*hash_pow_l%Mod) % Mod;
        if(ma.find(val) == ma.end())
            ma.insert( {val, string(s+i+1-l, s+i+1)} );
        else{
            t1 = ma[val], t2 = string(s+i+1-l, s+i+1);
            break ;
        }
    }
    cout << t1 << endl; cout << t2 << endl;
    val = val2 = 0;
    for(int i = 0; i < t1.size(); i++){
        val = (val*base+t1[i]-'a'+1)%Mod;
        val2 = (val2*base+t2[i]-'a'+1)%Mod;
        cout <<i << ':' << val <<' '<<val2 << endl;
    }
    return 0;
}
View Code

 

2.特征序列

假设抛一枚标准的硬币n次,最长连续正面的序列的期望长度有多长?

答案与lgn等阶, O(log)。

 

建堆的复杂度是O(n)

计数排序:先统计一遍前缀和,然后逆序插入。

基数排序最好从低有效位开始,从高有效位开始需要分段递归。

================================================

快排期望复杂度的证明:http://www.cnblogs.com/fengty90/p/3768827.html

================================================

经典算法题集锦

1. 数组a是一个不下降数组。将数组a循环移位后,求某个数出现的最左位置。O(logn)时间复杂度, O(1)空间复杂度。提示:差分

2. 一个数组,其中某个数出现的次数超过了一半。求该数。O(n)时间复杂度, O(1)空间复杂度。扩展:在一个数组中找出出现次数超过n/3的数字。

3. 给定一个未排序数组。求该数组在排完序后,相邻两数差的最大值。 O(nlogn)时间复杂度,O(n+k)时间复杂度, O(n)时间复杂度。提示:桶排序

4 给定一个未排序数组。求该数组在排完序后,连续数字的最大长度。O(nlogn)复杂度,O(n)复杂度。(hashset后,对每个数组中的数字,找其所在段的最大长度,找的同时删去找到的数).

5. 完美洗牌算法。将一个原始数组进行置换排序。

6. 二叉排序树:在一棵排序二叉树上找与给定值的差值第k小的值。O(nlogn)? O(klogn)? O(k+logn)!

7. 给定一个长度为n的有序数组A,找到离目标数m最近的一个长度为k的区间。 O(k+logn)? O(lognlogA)?O(logn+logk)!

8.给定一个长度为n的无序数组,任意两数不同。请找到任意一个局部最小值。O(logn)  

   拓展:给定一个n*n的无序二维数组,任意两数不同,请找到一个四联通的局部最小值。O(n) 提示:十字架划分,找到最小值,然后再选择更小的子块接着找,同时保留之前找的最小值。

posted @ 2017-01-02 17:13  我在地狱  阅读(677)  评论(0编辑  收藏  举报