显示标签为“算法”的博文。显示所有博文
显示标签为“算法”的博文。显示所有博文

星期三, 十月 25, 2006

基本算法连载(14)-BST(Binary Search Tree)的笔记

插入实现(传指针地址的地址):

void InsertNode(struct node **node_ptr, struct node *newNode) {
struct node *node = *node_ptr;
if (node == NULL)
*node_ptr = newNode;
else if (newNode->value <= node->value)
InsertNode(&node->left, newNode);
else
InsertNode(&node->right, newNode);
}

删除节点:

void DeleteNode(struct node*& node) {
struct node*&amp; temp = node;
if (node->left == NULL) {
node = node->right;
delete temp;
} else if (node->right == NULL) {
node = node->left;
delete temp;
} else {
// Node has two children - get max of left subtree
temp = node->left;
while (temp->right != NULL) {
temp = temp->right;
}
node->value = temp->value;
DeleteNode(temp);
}
}

星期日, 十月 22, 2006

基本算法连载(13)-递归程序转变成非递归

用堆栈实现递归其实并没有消除递归,只不过人工做了本来由编译器做的事情。真正的非递归是指运算时所需要的空间是常数,即所需空间与问题的输入规模无关。并非所有递归都可以转换成非递归,比如著名的Ackmann函数。
递归转非递归方法:
(1)用一般公式直接代替。象经常看到的斐波那契数列,它就存在通项公式,而无需采用递归实现。
(2)使用栈来实现
(3)通过Cooper变换、反演变换将一些递归转化为尾递归,从而迭代求出结果
至于什么是Cooper变换,反演变换,这可得好好研究数学。这里贴两个变换实例。

计算阶乘的递归实现(用Haskell实现的,谁都看得懂):f x = if x = 0 then 1 else f(x-1)*x;
第一步:转变成尾递归

int G(int x, int y)
{
int x1, y1;

if (x == 1) {
return 1 * y;
} else {
x1 = x - 1;
y1 = x *y;
return G(x1, y1);
}
}

int f(int x)
{
return G(x, 1);
}

第二步:转变成迭代

int G(int x, int y)
{
int x1, y1;

loop:
if (x == 1) {
return 1 * y;
} else {
x1 = x - 1;
y1 = x *y;
x = x1
y = y1;
goto loop;
}
}


求斐波那契值:
f x |x==0 =0
|x==1 =1
|otherwise = f (x-1)+f (x-2)

第一步:转变成尾递归
int fib(int n){
if(n==0)
return 0;
return _fib(n,1,0);
}

int _fib(int n,int f1,int f2){
if(n==1)
return f1;
return _fib(n-1,f1+f2,f1);
}

第二步:转变成迭代(略)

星期六, 十月 21, 2006

基本算法连载(12)-顺序查找的两个实现

顺序表的实现,天下人都知道,最最简单的一种,不过我还是贴出两个实现,大家看看:

int search(int a[],int key,int length){
int i;
for(i=length-1;i>=0;i--){
if(a[i]==key)
return i;
}
return -1;
}

/*
* 实际数组元素是从1号位置起开始存储,0号位置存储key
*/
int search(int a[],int key,int length){
int i;
a[0] = key;
for(i=length;!(a[i]==key);i--);
return i;
}

由此,想到了字符串的拷贝实现:

for(i=0;0!=(dst[i]=src[i]);i++);

星期一, 十月 16, 2006

基本算法连载(11)-两个基本概念:in-place和tail-end recursion

平时看算法,经常碰到in-place algorithm和tail-end recursion两个概念。今天终于了解了这两个概念。
In-place算法:The input is usually overwritten by the output as the algorithm executes.函数语言是不鼓励或支持in-place算法的,它把overwriiten当作side effect。函数语言,听过不少,没有学过,有时间得学学。
代码:

int sum(int n){
int i;
int sum = 0;
for(i=1;i<=n;i++){
sum = sum+i;
}
return sum;
}

此处的sum就被overwritten,可以算作in-place算法。

Tail-end recursion(tail recursion):函数所做的最后一件事情是一个函数调用,被称作尾部调用(tail-call)。使用尾部调用的递归程序称为尾部递归。tail-recursion是很容易转变成iteration的。在把尾部递归程序转变成非递归程序时,我们就有了理论保证。尾部调用是可以进行优化的:在尾部进行函数调用时使用一个栈结构覆盖当前的栈结构,同时保持原来的返回地址。
以下的代码展示的是一个更一般化的tail recursion,它先转变成熟悉的tail recursion,然后转变成iteration。看惯了Java代码,看这个还有点不习惯。
代码:

#include <stdlib.h>
typedef struct list{
int value;
struct list* next;
}list;

//----------------------------------
//一般化的tail-recursion
list* f(list* input){
list* head;
if(input == NULL){
head = NULL;
}else{
head = (list*)malloc(sizeof(list));
head->value = input->value;
head->next = f(input->next);
}
return head;
}

//------------------------------------
//熟悉的tail-recursion
void fprime(list* input,list** p){
if(input == NULL){
*p = NULL;
}else{
*p = malloc(sizeof(list));
(*p)->value = input->value;
fprime(input->next,&(*p)->next);
}
}

list* f1(list* input){
list* head;
fprime(input,&head);
return head;
}

//------------------------------------
//iteration
list* f2(list*input){
list* head;
list** p;
p = &head;
while(input != NULL){
*p = (list*)malloc(sizeof(list));
(*p)->value = input->value;
input = input->next;
p = &(*p)->next;
}
*p = NULL;
return head;
}



星期四, 十月 12, 2006

基本算法连载(10)-模式匹配之BM(Boyer-Moore)

周末两天被BM算法折磨的要死。《a fast string search algorithm》论文中提到的算法思想倒是理解的差不多,但网上(http://www-igm.univ-mlv.fr/~lecroq/string/node14.html#SECTION00140)给出的实现可就是看不懂。通过Baidu,Google一搜,可以看到很多Boyer-Moore的实现。但绝大部分实现都是简化版本,只考虑了bad-character shift,而忽略了good-suffix shift。
它思想的源泉是从右向左匹配字符串将获得更多有用的信息。The algorithm precomputes two tables to process the information it obtains in each failed verification: one table calculates how many positions ahead to start the next search based on the identity of the character that caused the match attempt to fail;the other makes a similar calculation based on how many characters were matched successfully before the match attempt failed.其中前一个称作bad-character shift,后一个称作good-suffix shift。想详细了解Boyer-Moore的思想,我极力推荐看作者发表的论文,它比网上对这个算法介绍的文章容易理解的多。
处理主串为n,模式串为m的情况,此算法的最好表现是:n/m。在这种情况下,只有模式串中的最后一个字符需要比较。不相等,马上可以跳过m个字符。从这里可以看出此算法一个违反直觉的性质:模式串越长,搜索越快。
最优的代码实现(没看懂,明天贴个我自己改的):
void preBmBc(char *x, int m, int bmBc[]) {
int i;

for (i = 0; i < ASIZE; ++i)
bmBc[i] = m;
for (i = 0; i < m - 1; ++i)
bmBc[x[i]] = m - i - 1;
}


void suffixes(char *x, int m, int *suff) {
int f, g, i;

suff[m - 1] = m;
g = m - 1;
for (i = m - 2; i >= 0; --i) {
if (i > g && suff[i + m - 1 - f] < i - g)
suff[i] = suff[i + m - 1 - f];
else {
if (i < g)
g = i;
f = i;
while (g >= 0 && x[g] == x[g + m - 1 - f])
--g;
suff[i] = f - g;
}
}
}

void preBmGs(char *x, int m, int bmGs[]) {
int i, j, suff[XSIZE];

suffixes(x, m, suff);

for (i = 0; i < m; ++i)
bmGs[i] = m;
j = 0;
for (i = m - 1; i >= -1; --i)
if (i == -1 || suff[i] == i + 1)
for (; j < m - 1 - i; ++j)
if (bmGs[j] == m)
bmGs[j] = m - 1 - i;
for (i = 0; i <= m - 2; ++i)
bmGs[m - 1 - suff[i]] = m - 1 - i;
}


void BM(char *x, int m, char *y, int n) {
int i, j, bmGs[XSIZE], bmBc[ASIZE];

/* Preprocessing */
preBmGs(x, m, bmGs);
preBmBc(x, m, bmBc);

/* Searching */
j = 0;
while (j <= n - m) {
for (i = m - 1; i >= 0 && x[i] == y[i + j]; --i);
if (i < 0) {
OUTPUT(j);
j += bmGs[0];
}
else
j += MAX(bmGs[i], bmBc[y[i + j]] - m + 1 + i);
}
}


基本算法连载(9)-模式匹配之KMP(Knuth-Pratt-Morris)

KMP算法,在《数据结构》课上听过,似是非懂,读完大学后全忘光了。Brute-Force算法,简单,谁都知道。从主串S的第pos个字符起与模式串进行比较,匹配不成功时,从主串S的第pos+1个字符重新与模式串进行比较。如果主串S的长度是n,模式串长度是m,那么Brute-Force的时间复杂度是o(m*n)。最坏情况出现在模式串的子串频繁出现在主串S中。虽然它的时间复杂度为o(m*n),但在一般情况下匹配时间为o(m+n),因此在实际中它被大量使用。
前几日,重新拾起了KMP算法,然后向MM讲解之。KMP的主要思想是:每当一趟匹配过程中出现字符比较不等时,不需回溯主串S的指针,而是利用已经得到的“部分匹配”结果将模式串向右“滑动”尽可能远的一段距离后,继续进行比较。
模式串到底向右滑动多少,在KMP算法中是用一个数组来存储的。针对模式串中的每个索引j,都将有一个对应的值。此值的含义为模式串中位置从0到j-1构成的串中所出现的首尾相同的子串的最大长度加1。
下面给出具体实现:
/*
* n is the length of text,while m is the length of pattern.
* And pos which is zero-indexed is the start point of search.
*/
int kmp(char* text,int n,char *pattern,int m,int pos){
int i,j;
//Generate the array of next
int* next = (int*)malloc(m*sizeof(int));
i = 0;
j = -1;
next[i] = j;
while(i<m){
if((j==-1) || (pattern[i]==pattern[j])){
i++;
j++;
if(pattern[i]!=pattern[j])
next[i] = j;
else
next[i] = next[j];
}else{
j = next[j];
}
}

int k = 0;
for(k=0;k<m;k++)
printf("next:%d\n",next[k]);

//Search
i = pos;
j = 0;
while(i<n&&j<m){
if((j==-1) || (text[i]==pattern[j])){
i++;
j++;
}else{
j = next[j];
}
}
if(j==m)
return i-m;
else
return 0;
}
KMP算法的时间开销包括两部分,一个是求next数组元素的值,此时的时间开销是o(m);一个是搜索,此时的时间开销是o(n)。因此,它的时间复杂度是o(m+n)

星期四, 九月 21, 2006

基本算法连载(8)-Library Sort(gapped insertion sort)

特色:Library sort优于传统的插入排序(时间复杂度为O(n^2)),它的时间复杂度为O(nlogn),采用了空间换时间的策略。
思想:一个图书管理员需要按照字母顺序放置书本,当在书本之间留有一定空隙时,一本新书上架将无需移动随后的书本,可以直接插空隙。Library sort的思想就源于此。
实现:有n个元素待排序,这些元素被插入到拥有(1+e)n个元素的数组中。每次插入2^(i-1)个元素,总共需要插logn趟。这2^(i-1)个元 素将被折半插入到已有的2^(i-1)个元素中。因此,插入i趟之后,已有2^i个元素插入数组中。此时,执行rebalance操作,原有处在(1+ e)2^i个位置的元素将被扩展到(2+2e)2^i个位置。这样,在做插入时,由于存在gap,因此在gap未满之前无需移动元素。
具体代码

/*
* length:待排序元素个数
* elements:待排序数组
* factor:常数因子
*/
void librarySort(int length,float factor,int elements[]){
int i,j;
//扩展后的数组长度
int expandedLen = (int)((1+factor)*length);
int* orderedElem = (int*) malloc(expandedLen*sizeof(int));

//标志gap
int flag = 1<<31;
for(i=0;i<expandedLen;i++){
orderedElem[i] = flag;
}

int index = 1;
int numOfIntercalatedElem = 1;
orderedElem[0] = elements[0];

while(length>numOfIntercalatedElem){
//第i次插入2^(i-1)个元素
for(j=0;j<numOfIntercalatedElem;j++){
//待插入元素为elements[index]
//------------折半插入---------------
int mid;
int low = 0;
int high = 2 * numOfIntercalatedElem - 1;
while(low <= high){
mid = (low + high)/2;

int savedMid = mid;
//如果mid所在位置为gap
while(orderedElem[mid] == flag){
if(mid == high){
//当向右遍历没有找到元素值时,改成向左遍历
mid = savedMid - 1;
while(orderedElem[mid] == flag){
mid--;
}
break;
}
mid++;
}

if(elements[index] > orderedElem[mid]){
low = mid + 1;
//缩小范围
while(orderedElem[low] == flag){
low = low+1;
}
}else{
high = mid - 1;
}
}

//把elements[index]插入到orderedElem[high+1]
//当位置为空,没有存储元素值时...
if(orderedElem[high+1] == flag){
orderedElem[high+1] = elements[index];
}else{
//位置非空,首先往前挪动元素,如果前面已满,向后挪动元素
int temp = high+1;
while(orderedElem[temp] != flag){
temp--;
if(temp < 0){
temp = high+1;
break;
}
}

//向后移动
while(orderedElem[temp] !=flag){
temp++;
}

while(temp < high){
orderedElem[temp] = orderedElem[temp+1];
temp++;
}

while(temp > high+1){
orderedElem[temp] = orderedElem[temp-1];
temp--;
}

orderedElem[temp] = elements[index];
}
//---------------------------------
index++;
if(index == length){
break;
}
}

numOfIntercalatedElem *=2;
int generatedIndex;
//Rebalance...
for(j=numOfIntercalatedElem;j>0;j--){
if(orderedElem[j] == flag){
continue;
}
//原数组元素从i处移到2i处
generatedIndex = j*2;
if(generatedIndex >= expandedLen){
generatedIndex = expandedLen - 1;
if(orderedElem[generatedIndex] != flag){
break;
}
}
orderedElem[generatedIndex] = orderedElem[j];
orderedElem[j] = flag;
}
}
//测试输出
for(i=0;i<expandedLen;i++){
printf("%d\n",orderedElem[i]);
}

}

星期一, 九月 18, 2006

基本算法连载(7)-Skip List(下)

基本算法连载(6)-Skip List(上)

Skip List号称性能与BST(Binary Sort Tree)树有得一拼,于是把它翻了个底朝天。代码是阐述其思想的最好方式,那我们还是看看它的具体实现(采用Java语言)
public class SkipList {

public static final int NOT_FOUND = -1;
public static final int HEADER_KEY = -2;
public static final int NIL_KEY = Integer.MAX_VALUE;

// optimum probability
public static final float OPT_PROB = 0.25f;
private float myProbability;
// probability to increase level
private int myMaxLevel;
// upper bound of levels
private int myLevel;
// greatest level so far
private SkipListElement
myHeader; // the header element of list

/*
* Constructs a new skip list optimized for the given
* expected upper bound for the number of nodes.
*/

public SkipList(long maxNodes) {
// probability set to 0.25 and maximum level
// of list is depending on expected number of nodes
// (see paper for mathematical background)
this(OPT_PROB,(int)Math.ceil(Math.log(maxNodes)/Math.log(1/OPT_PROB))-1);
}

public SkipList(float probability, int maxLevel) {
myLevel = 0;
myProbability = probability;
myMaxLevel = maxLevel;
// generate the header of the list
myHeader = new SkipListElement(myMaxLevel,HEADER_KEY, 0);

// append the "NIL" element to the header
SkipListElement nilElement = new SkipListElement(myMaxLevel, NIL_KEY, 0);
for (int i=0; i<=myMaxLevel; i++) {
myHeader.forward[i] = nilElement;
}
}

/*
* Generates with help of randomizer the level of a new element.
* The higher a level, the less probable it is (see paper).
* Levels begin at 0 (not at 1 like in the paper).
*/
private int generateRandomLevel() {
int newLevel = 0;
while (newLevel<myMaxLevel &&Math.random()<myProbability ) {
newLevel++;
}
return newLevel;
}

/*
* Inserts a new node into the list.
* If the key already exists, its node is updated to the new value.
*/
public void insert(int searchKey, int value) {
// update pointers to next elements on each level and
// levels run from 0 up to myMaxLevel.
SkipListElement[] update = new SkipListElement[myMaxLevel+1];

// init "cursor" element to header
SkipListElement element = myHeader;

// find place to insert the new node
for (int i=myLevel; i>=0; i--) {
while (element.forward[i].key <searchKey) {
element = element.forward[i];
}
update[i] = element;
}

element = element.forward[0];

// element with same key is overwritten
if (element.key == searchKey) {
element.value = value;
}else{
// or an additional element is inserted
int newLevel = generateRandomLevel();
// element has biggest level seen in this list,update list
if (newLevel > myLevel) {
for (int i=myLevel+1;i<=newLevel; i++) {
update[i] = myHeader;
}

myLevel = newLevel;
}

// allocate new element:
element = new SkipListElement(newLevel,searchKey, value);
for (int i=0; i<=newLevel; i++) {
element.forward[i] = update[i].forward[i];
update[i].forward[i] = element;
}
}

}

/*
* Search for a given key in list. You get the value associated
* with that key or the NOT_FOUND constant.
*/

public int search(int searchKey) {
// init "cursor"-element to header
SkipListElement element = myHeader;

// find element in list:
for (int i=myLevel; i>=0; i--) {
SkipListElement nextElement = element.forward[i];
while (nextElement.key < searchKey) {
element = nextElement;
nextElement = element.forward[i];
}
}

element = element.forward[0];
if (element.key == searchKey) {
return element.value;
}else {
return NOT_FOUND;
}

}

public void delete(int searchKey) {
// update holds pointers to next elements of each level
SkipListElement update[] = new SkipListElement[myMaxLevel+1];

// init "cursor"-element to header
SkipListElement element = myHeader;

// find element in list
for (int i=myLevel; i>=0; i--) {
SkipListElement nextElement = element.forward[i];
while (nextElement.key < searchKey) {
element = nextElement;
nextElement = element.forward[i];
}
update[i] = element;
}

element = element.forward[0];
// element found, so rebuild list without node
if (element.key == searchKey) {
for (int i=0; i<=myLevel; i++) {
if (update[i].forward[i] == element) {
update[i].forward[i] = element.forward[i];
}
}

// element can be freed now
element = null;

// maybe we have to downcorrect the level of the list
while (myLevel>0&& myHeader.forward[myLevel].key==NIL_KEY){
myLevel--;
}
}
}

/*
* inner class
*/

private class SkipListElement {
int key;
int value;
int level;

// array of forward pointers
SkipListElement forward[];

public SkipListElement(int level, int key, int value) {
this.key = key;
this.value = value;
this.level = level;
forward = new SkipListElement[this.level+1];
}
}

}

星期四, 九月 07, 2006

基本算法连载(5)-Bubble sort

冒泡排序基于交换的思想,简单,易于实现,时间复杂度为O(n^2)。它最多只能充当排序算法的一种演示,不会运用到实际开发中,因为它声名狼藉。看看算法界的泰 斗Donald Knuth是怎么说的,“The bubble sort seems to have nothing to recommend it, except a catchy name and the fact that it leads to some interesting theoretical problems.”既然如此,冒泡排序的问题出在哪里呢? “Large elements at the top of the list do not pose a problem, as they are quickly swapped downwards. Small elements at the bottom, however, move to the top extremely slowly. This has led to these types of elements being named rabbits and turtles, respectively.”
针对问题根源,人们找到了一些缓解方案。一个是bidirectional bubble sort,一个是comb sort。
  • bidirectional bubble sort:一趟排序到达数列尾部时,从尾部开始遍历。到达头部后,接着又从头部向尾部遍历,如此反复,完成排序。此时时间复杂度仍然是O(n)。
  • comb sort:进行排序时,每次比较交换的元素跨度将不局限于1。当turtle都移到头部时,元素跨度将变成1,完成最终的排序。元素跨度值的设置对排序性 能有重大影响,经验表明,跨度最先选择为数列长度除以1.3所获得的商值是最优的。此种算法的时间复杂度为O(nlgn)。代码示例如下:
private static int newGap(int gap){
gap = gap * 10 /13;
if(gap == 9 || gap == 10)
gap = 11;

if(gap < 1){
return 1;
}else{
return gap;
}

}

public static void sort(int a[]){
int gap = a.length;
boolean swapped;
do{
swapped = false;
gap = newGap(gap);

for(int i=0;i<a.length-gap;i++){
if(a[i] > a[i+gap]){
swapped = true;
int temp = a[i];
a[i] = a[i+gap];
a[i+gap] = temp;
}
}

}while(gap>1 || swapped);

}

星期三, 九月 06, 2006

基本算法连载(4)-Google的一道面试题

  • 题目: 在一个集合S中寻找最大的C,使A+B=C,且A,B,C均在集合当中,时间空间复杂度最低。
  • 解题思路:
  • 方案一:
    1. 对集合进行快速排序,时间复杂度为O(nlgn),空间复杂度为O(lgn);
    2. 从最后一个元素开始,依次调用“在一个有序集合中寻找两个数的和等于给定值的算法”。一次调用需要O(n),n个元素需要O(n^2)。
    3. 总体来看,此方案的时间复杂度为O(n^2),空间复杂度为O(lgn)。
  • 方案二:
    1. 对集合进行快速排序,时间复杂度为O(nlgn),空间复杂度为O(lgn);
    2. 用排序后的n个数(a[0],a[1],a[2],...a[n-1])构造n×n的两两和矩阵,此时有m[i][j]=a[i]+a[j],m[i] [j]<=m[i][j+1],m[i][j]<=m[i+1][j],在此矩阵中搜索一个数是否存在,时间复杂度为O(n+n),所以n个数的时间复杂度为O(n^2)。矩阵元素可以直接通过a[i]与a[j]相加动态获得,所以不需要额外开辟空间。
    3. 总体来说,此方案的时间复杂度为O(n^2),空间复杂度为O(lgn)。
    4. 具体搜索方法为:对于一个给定的数a,沿着对角线走下去,遇到第一个大于a的数,然后往上走,如果找到就OK,否则不存在。

星期一, 九月 04, 2006

基本算法连载(3)-3个基本性质

  • 性质1:一颗二叉树(严格的二叉树,即没有度为1的节点),有N个内部节点,那么它将有N+1个外部节点。
采用归纳法证明:
  1. 如果只有一个外部节点,即单独的一个根节点,那么内部节点数为0,结论显然成立。
  2. 假设有N-1个内部节点时,结论成立。那么二叉树拥有N个内部节点时,左子树有k个内部节点,右子树有N-k-1个内部节点,此外包括一个根节点。由于左 右子树的节点数都小于N,所以由前面假设可得左子树有k+1个外部节点,右子树有N-k个节点,总共有k+1+(N-k)个节点,即N+1个节点。
  3. 由性质1可得,一颗有N个外部节点的二叉树需要用数组存储,那么需要申请一个拥有2N-1个元素的数组。
  • 性质2:在一个已排序的序列中寻找和为给定数值的两个数,可在O(n)时间内完成。
简单示例:
#include <stdio.h>
int main(int argc,char*argv[]){
//排好序的数组序列
int a[10] = {1,2,3,4,5,6,7,8,9,10};
//给定值
int value = 21;
int i = 0,j = 9;
while(1){
if(a[i] + a[j] == value){
break;
}else{
if(i >= j){
printf("%s\n","Not found!");
exit(1);
}
}
if(a[i] + a[j]<value){
i++;
}else{
j--;
}
}
printf("%d\n%d",i,j);
return 0;
}

  • 性质3:在一个无序序列中寻找和为给定数值的两个数,可在O(nlgn)时间内完成。
  1. 采用快速排序对无序序列进行排序,时间复杂度为O(nlgn)。
  2. 由性质2可知,时间复杂度为O(n)。

星期二, 八月 29, 2006

基本算法连载(2)-摔xbox

  • 题目
You have been given 2 special, extremely rugged Xboxes. You are in an office building that is 100 stories high. Using the fewest possible number of drops from windows in your office building, determine the highest floor you can drop an Xbox from and have it survive: for example, they might be able to take the drop from the 30th floor, but not the 31st. You can break both Xboxes in your search. State the worst case number of drops needed and explain how you arrived at that answer.
你在一幢100层的办公楼里上班,现在给你两台xbox(已经特意捆绑包扎好),要求你用尽可能少的试摔次数来判断xbox摔不坏的最高楼层层数。比方说,从30层丢下来没问题,但从31层丢下来就不保了。在摸索过程中,允许把两台xbox都砸烂。在最坏情况下,使得试摔次数尽可能小。
  • 分析
  1. 问题答案必须是在最坏情况下,时间复杂度最小。
  2. 最容易想到的就是二分法,三分法,多分法,但它们并不符合要求。比如:在50层丢一个,被砸烂。然后在25层丢另外一个,同样被砸烂。此时根本无从找到问题答案。
  3. 第二个想到的方法就是分段。(1)用第一个xbox寻找损坏区间;(2)用第二个xbox遍历该区间寻找损坏层。假定最优区间长度是x,则第一步最多要摔100/x(大于等于此值的最小整数)次,第二步最多摔x-1次,问题转换为求100/x+(x-1)的最小值。具体描述:以10层楼为一个区间,先摔第一个,以确定摔坏的区间,然后再用另一个在这个区间内从最低的楼层摔,从而找到所要求层数,这种方法最多要摔19次。
  4. 在“分段”方法中,段区间都是相等的。如果段区间不等,是不是可以找到最优的方法呢?假设最终答案至少要摔n次,那第一次顶多从n层楼摔。如果坏了,第二 个从1至n-1层最多摔n-1次就可以判断了;如果没坏,那么搜索范围改为[n+1,100],相当于搜索空间减少了n层。如果第一次没坏,第二次顶多再 减少n-1的搜索空间。依次类推,当第n次时顶多再减少1的搜索空间。n次之后,顶多排除n+(n-1)+(n-2)+...+1 = n*(n+1)/2个楼层。只要n*(n+1)/2 >= 100就可以了,满足这个条件的最小n为14。具体描述:从14楼丢一个,如果碎了,从1楼开始到13楼为止丢另一个,不碎,在14+13楼丢;...
  • 证明
给一个包含n个元素的数组,{a[1],...,a[n]},a[1]+a[2]+...+a[n]=100。那么如果第k次摔坏了第1台xbox ,那么此时最坏情况下总共需要的次数是k+a[k]-1我们把这个记为cost[k]。此时,问题可以重新描述为:找到一个满足上述条件的数组,{a[1],...,a[n]} ,使得 max(cost[k])最小。
sigma( cost[k] )
= (1+a[1]-1) + (2+a[2]-1) + (3+a[3]-1) ... + (n+a[n]-1)
= (1+2+3+ ... +n) + (a[1]+a[2]+a[3]+ ... +a[n]) - n
= n*(n+1)/2 + 100 - n
这个式子是个常量仅与n有关,与{a[1],...,a[n]}的具体数值无关。同时,不难看出 max( cost[k] ) >= sigma( cost[k] ) / n (最大值总是大于平均值),而且仅当 cost[1] =cost[2] = ... cost[n] 的时候,max...可以取到这个最小值。因此,对于任意给定的n,满足 cost[1]= cost[2] = ... cost[n]的解就是最优解。也就是说k+a[k]-1=1+a[1]-1,从而a[k]=a[1]-k+1 。那么a[2]=a[1]-1,a[3]=a[1]-2=a[2]-1...

参考:(1) 摔xbox的题目

找病狗

  • 题目
村子中有50个人,每人有一条狗。在这50条狗中有病狗(这种病不会传染)。于是人们就要找出病狗。 每个人可以观察其他的49条狗,以 判断它们是否生病(如果有病一定能看出来),只是自己的狗不能看。观察后得到的结果不得交流,也不能通知病狗的主人。主人一旦推算出自己家的是病狗就要枪 毙自己的狗(发现后必须在一天内枪毙),而且每个人只有权利枪毙自己的狗,没有权利打死其他人的狗。 第一天大家全看完了,但枪没有响,第二天仍没有枪响。到了第三天传来一阵枪声,问村里共有几条病狗,如何推算得出?
  • 答案
3条病狗
  • 分析
  1. 假设有n条病狗,那么就有50-n个人的狗不是病狗。第一天大家枪都没有响, 那么n必然大于1 。因为如果n = 1.那么必然有一个人(病狗的主人)看到49条好狗,那样他就会枪毙自己的狗。所以这里 n >= 2。
  2. 到了第二天。大家都清楚了n >=2 ,但是还是没有开枪。这个时候必然有 n > 2,因为如果n == 2的话 .那两个病狗的主人看到都是一条病狗,他们会明白还有一条病狗就是自己那条,所以就会有人开枪。但是没人开枪,说明n >= 3。
  3. 到了第三天,有人开枪了。因为 在前两天只能确定 n >=3 , 因为有人开枪了,那么开枪的那个人肯定能断定自己那条是病狗,并且他知道 n >=3 。 所以他肯定只看到了 2只病狗,才能确定他自己那只肯定是病狗。以此来推,第几天开枪,就有几只病狗。

基本算法连载(1)-顺序搜索与二分搜索

顺序搜索
Programmer每天都碰到顺序搜索,其code snippet:
/*
* 顺序遍历数组,搜索v值是否存在。如果存在,返回相应的位置索引,
* 否则返回-1
*/
int sequentialSearch(int a[],int v,int l,int r){
int i;
for(i = l;i <= r;i++){
if(v == a[i])
return i;
}
return -1;
}

分析:
  1. 假设数组拥有N个元素,对于不成功的搜索,总是搜索N个元素;对于成功的搜索,平均搜索次数为N/2。
  2. 假设每个数组元素被搜索到的概率相等,那么平均搜索次数可以这样计算:(1+2+...+N)/N = (N+1)/2
  3. 对于排序数组,相关性质不变。

二分搜索
code snippet:
/*
* 二分搜索,非递归实现
* 假设:数组元素已经按序排列
*/
int binarySearch(int a[],int v,int l,int r){
int m;
while(l<=r){
m = (l+r)/2;
if(v == a[m])
return m;

if(v > a[m]){
l = m + 1;
}else{
r = m - 1;
}
}
return -1;
}

/*
* 二分搜索,递归实现
*/
//dataForExperiment数组定义及初始化
int binarySearch(int v,int l,int r){
if(l > r){
return -1;
}
int m = (l + r)/2;
if(v == dataForExperiment[m]){
return m;
}else{
if(v > dataForExperiment[m]){
return binarySearch(v,m+1,r);
}else{
return binarySearch(v,l,m-1);
}
}
}
分析:
  1. 假设Tn表示在最坏情况下二分搜索需要比较的次数,那么有Tn=Tn/2+1,其中n>=2,T1=1
  2. 求解递归式,可以得到二分搜索检查元素个数永远不超过lgn+1。
  3. 数组必须是已经排好序的。