普通视图

发现新文章,点击刷新页面。
昨天以前极客中心

长期主义者什么意思?真正的高手都是长期主义

2020年10月13日 18:15

嗯,什么是真正的长期主义?你是否想过这个问题,是坚持一件工作几十年如一日的不动摇,还是死磕一项技能,不管如何惨淡都不放弃,又或者是坚持不懈的努力,脚踏实地,不搞投机。

长期主义就是长周期加坚持吗?如果真是如此,这些标准,我们身边的老好人全部满足,那为什么他们却过如此惨淡?

大家好,我是大白哥。今天我想和大家聊聊什么是真正的长期主义,什么叫真正的长期主义?

长期主义误区一:只盯着长远的目标,不计较当下的得失

极客白小飞-长期主义误区一

大多数人的第一个误区就是盯着长远的目标,不计较当下的得失。

然而,真正的长期主义是注重短期的,是需要快速迭代,小步快跑。因为外部的环境和你遭遇的问题在不断变化,任何目标都要根据环境的变化而调整。否则时代在变,你不变只会倒在浩荡大势的车轮下被碾压的粉碎。

这里还要引申到一个重要的理念,就是短周期。正确认识长期主义,你就能发现所有的长周期都是由多个短周期组成的。这些短周期的成败,决定了长周期的结果。 譬如我,2018年开始坚持做自媒体,和我一起开始做人很多,向我请教过的人也不少,但99%的人都放弃,不少人都说我没有你这样的天赋,我努力了很久都没有结果。

当一个时间周期达不到时,大多数人都会选择放弃。而这些种种失败的背后,问题其实并不出在天赋或者其他方面,而是大多数人并没有重视短周期,眼睛只是盯着长周期死磕。 而我却恰恰相反,并不像大多数人一样学着写好一篇又一篇文章,而是从写好一个句子开始,到写好一个标题,再从写好一个开头开始,接着到写好一个段落,最后才到写好一篇文章。 很多人一开始就盯着我要写好一篇文章,写了10篇,100篇。 和其他人对比起来深感差距很大,就放弃了。一开始你的方向和目标就错了,结果自然是错的。

长期主义误区二:一味的相信坚持的力量撞破南墙都不回头

长期主义的第二个误区是一味的相信坚持的力量撞破南墙都不回头。

极客白小飞-长期主义误区二

坚持是好事,但过往的视频里,我不止一次的提到概率论,按照公式来说就是:足够尝试的次数加一定的概率,再小的偶然都会出现必然的结果。

极客白小飞-概率论

我们常说时间会给我们答案,就是这个原理,很多时候就是如此。 当我们活得足够惨,做的选择足够多时,大数法则就会发生作用。到那个时候我们的命运就不再取决于一到两次的选择,而是整个系统。运气就不再是虚无缥缈的东西。

但问题来了,之所以坚持的原因是因为很多正确的选择,只有把时间拉长才能看到结果,而不是拉长时间,错误的选择也能开出奇迹的花朵。你首先要清楚你的抉择和方向是正确的,然后再坚持,在长时间的周期里才能将偶然变成必然,否则成功率0%的事情在10年里坚持1万字,结果还是0。

长期主义误区三:不清楚长期有多长

长期主义的第三个误区就是:不清楚长期有多长。

极客白小飞-长期主义误区三

我们嘴里总是说说长期,但你是否想过长期到底有多长,是5年、10年还是一辈子?这些答案都是错误的。长期主义是在符合一件事物的周期曲线里去努力,而不是盲目的去坚持。

譬如,梁宁老师之前提到过一个观点,有一对双胞胎在2010年一起大学毕业,一个加入了腾讯,一个进入了报社。7年之后,去腾讯的那位已经是年薪百万,而且满街都是挖他的猎头,投资人也在挖他,只要出来创业就给钱。去报社的那位,因为报社沉沦,他曾经寄托理想的整个产业都没了,一切都需要重来。

同样在一个长周期里,为什么两者的命运如此不同,答案其实就在周期曲线里,任何事物都有萌芽期、成长期、巅峰期、衰落期和终结期,好比生死轮回。

极客白小飞-长期主义的周期曲线

因此真正的长期主义是识别周期曲线的变化。然后像傻子一样的努力,而不是用努力去博不确定的未来,知道这点很重要。

长期主义总结

最后帮我们做一个总结。

第一,真正的长期主义不是只注重长周期,而是既注重长期,更注重短期,快速迭代,小步快跑。

第二,长期主义不等于坚持,你要选择正确的方向或者确定这个方向存在概率,然后把时间拉长。然后尝试才会有好结果,否则一定是事与愿违的。

第三,要清楚长期主义的长期有多长,根据自己选择的方向,识别未来的走向,不要与趋势作对。

以上就是我今天的分享,更多精彩内容,大家可以关注公号“大白技术控”,我们下期再见。

deno零基础入门教程

2020年10月9日 04:15

安装Deno

首先,我们需要安装好 Deno。

在Windows下安装Deno,有两种方式

  • git bash中用curl安装

具体命令是:

1
curl -fsSL https://x.deno.js.cn/install.sh | sh -s v1.0.0
  • 在PowerShell中使用chocolate包管理器安装

首先,你需要使用powershell安装好chocolate,具体操作如下:

以管理员权限打开powershell,然后输入以下命令:

1
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))

安装好chocolate后,可以直接在PowerShell中以管理员权限运行如下命令:

1
choco install deno

未完待续~

站长之家 seo查询

2020年9月30日 18:21

极客白小飞-站长之家

作为一枚中文站点的站长,站长之家的SEO工具我们是必须熟悉并掌握的。今天大白进行了全面的整理,分享给大家~

Chinaz站长后台工具

网站管理 - 站长工具

http://my.chinaz.com/ChinazToolSeo/WebsiteManage/Index

极客白小飞-站长之家-网站管理页

需要注意的是,免费账号最多只能提交一个网站。另外,如果你的站点是https的,在这里的一个站长验证是无法通过的,不过不影响其他功能的使用。

其中的免费功能有如下这些:

SEO监控

域名安全监控

工具中心 - 站长工具

http://my.chinaz.com/ToolMember/Member/Account

ALEXA中心 - 网站管理 - 站长工具

https://alexa.chinaz.com/usercenter/

极客白小飞-站长之家-Alexa用户中心

日常实用工具 - 站长工具

http://tool.chinaz.com/map.aspx

SEO查询

优化辅助

世界排名

权重查询

数据内参

百度关键词

360关键词

指数查询

域名备案

已备案域名

备案账号安全

IP 查询

同IP网站

域名

网站监控

安全状态

网站状态

网页相关

网页信息抓取

网站链接

反链/死链

短链

友情链接查询结果: http://link.chinaz.com/yanglr.github.io

百度自动推送不能用了吗?你错了,刚恢复了!

2020年9月16日 04:05

百度搜索线上公开课 的第二期公开课中百度的产品运营小姐姐 李文菊 提到自动推送push.js接口过一段时间会恢复上线的。

百度自动推送恢复-极客白小飞

百度自动推送恢复-极客白小飞

视频回放地址: http://mbd.baidu.com/webpage?type=live&action=liveshow&source=h5pre&room_id=3789761765, 就是在时间点03:04的。

这不,今天就恢复上线了~

今天进入 百度搜索资源平台 时,发现”自动推送”的入口已恢复,有图有真相!

百度自动推送恢复-极客白小飞

具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
(function(){
    var bp = document.createElement('script');
    var curProtocol = window.location.protocol.split(':')[0];
    if (curProtocol === 'https') {
        bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
    }
    else {
        bp.src = 'http://push.zhanzhang.baidu.com/push.js';
    }
    var s = document.getElementsByTagName("script")[0];
    s.parentNode.insertBefore(bp, s);
})();
</script>

各位小伙伴,赶紧加回来吧,你的网站收录情况一定会越来越好喔~

必应API接口node.js版

2020年9月15日 18:21

近期,在研究百度、必应、API等的url提交API时,发现有用Go语言做工具的大佬的分享 利用 API 自动向搜索引擎提交网址(Go语言版) - pyList

其中提到bing API提交方法,并给出了Go语言代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Bing() {
    sUrl := "https://ssl.bing.com/webmaster/api.svc/json/SubmitUrl?apikey=xxxxxxxx"
    buf := bytes.NewBufferString(`{
"siteUrl":"https://pylist.com",
"url":"https://pylist.com/t/1581940902"
}`)
    req, err := http.NewRequest("POST", sUrl, buf)
    if err != nil {
        return
    }
    req.Header.Set("Content-Type", "application/json; charset=utf-8")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return
    }
    defer resp.Body.Close()
}

可以保存为: bing-push.go, 然后在本地执行哈~

而相比于go语言,我本人对node.js更熟悉一点~

必应API接口-单条提交

1
2
3
4
5
6
7
8
9
10
11
12
13
var request = require('request');
var options = {
    uri: 'https://ssl.bing.com/webmaster/api.svc/json/SubmitUrl?apikey=' + 'xxx', /* xxx需替换为你的key */
    method: 'POST',
    json: {
        "siteUrl": "http://yanglr.github.io", /* 替换为你的站点,并且在Bing站长平台中验证过权限 */
        "url": "http://yanglr.github.io/link.html"   /* 替换为你需要推送的url */
    }
};

request(options, function (error, response, body) {
    console.log("Bing response: ", body)
});

登录必应站长后台https://www.bing.com/webmasters,点右上角的设置按钮(齿轮⚙),找到你的key:

Step 1:

大奔SEO_极客白小飞

Step 2:

大奔SEO_极客白小飞

然后将上述代码中的xxx替换为你的key。

先保存文件为: bing-SingleSumbit.js,

然后在当前目录下打开命令行,输入 npm install request,

接下来改好key, siteurl, url等值后,就可以在命令行中运行:

1
node bing-SingleSumbit.js

必应API接口-批量提交

批量提交 - 版本1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var request = require('request');
var myJson = {
    "siteUrl": "http://yanglr.github.io",
    "urlList": [
        "http://yanglr.github.io/link.html",
        "http://yanglr.github.io/about.html",
        "http://yanglr.github.io/blog/"
    ]
};
request({
    url: 'https://ssl.bing.com/webmaster/api.svc/json/SubmitUrlbatch?apikey=' + 'xxx', /* xxx需替换为你的key */
    method: "POST",
    json: true,   // <--Very important!!!
    body: myJson
}, function (error, response, body) {
    console.log(body);
});

我记得这里有个跨域的问题, 设置 json: true 即可解决。

先将代码保存为: bing-BatchSumbit.js.

运行方法,同上~

批量提交 - 改进1

在上一版本的基础上可以改进,即:把多条url先按行放进link.txt中,然后读取处理~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var fs = require('fs');
var readline = require('readline');
var path = require('path')

function readFileToArr(fReadName, callback) {
    var arr = [];
    var readObj = readline.createInterface({
        input: fs.createReadStream(fReadName)
    });

    readObj.on('line', function (line) {
        arr.push(line);
    });
    readObj.on('close', function () {
        console.log('readLine close....');
        callback(arr);
    });
}

// var urlsFile = path.resolve(__dirname, 'links.txt').replace(/\\/g, '/');  // For Windows
var urlsFile = path.resolve(__dirname, '..', 'nodejs', 'links.txt'); /* 兼容 Windows/Linux, 这里nodejs为上级文件夹名 */

readFileToArr(urlsFile, function (arr) {
    var request = require('request');
    var myJson = {
        "siteUrl": "http://yanglr.github.io",
        "urlList": arr
    };
    
    request({
        url: 'https://ssl.bing.com/webmaster/api.svc/json/SubmitUrlbatch?apikey=' + 'xxx',
        method: "POST",
        json: true,   // <--Very important!!!
        body: myJson
    }, function (error, response, body) {
        console.log(body);
    });
});

保存文件为: bing-BatchSumbit2.js

命令行中用cd命令切换到当前目录,然后依次输入:

1
2
3
npm install fs
npm install readline
npm install path

改好key, siteurl, url等值后,并在当前目录创建文件links.txt并填入需要推送的多条url,就可以在命令行中运行:

1
node bing-BatchSumbit2.js

批量提交 - 改进2

上一版本的代码中,links.txt的内容是手动添加的,那我们可不可以从sitemap.xml获取并直接转换为.txt供后面使用呢?当然可以,于是另外写了一段node.js代码做这个事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var fs = require('fs');
var request = require('request');
const cheerio = require('cheerio');

request('https://geekzl.com/sitemap.xml', function (error, response, html) {
    if (!error && response.statusCode == 200) {
        var $ = cheerio.load(html, {
            xmlMode: true
        });

        textFile = 'myLink.txt';
        fs.open(textFile, 'wx', (err, fd) => {
            if (err) {
                if (err.code === 'EEXIST') {
                    console.error('myfile already exists');
                    
                    fs.unlinkSync(textFile);  // Remove file
                }
            }

        });

        const nodes = $('loc');
        var arr = [];
        
        for (i = 0; i < nodes.length; i++) {
            arr[i] = nodes[i].firstChild.data;

            fs.appendFile(textFile, arr[i] + '\r\n', function (err) {
                if (err) {
                    console.error('One line converted failed.'); // append failed
                } else {
                    // console.error('One line converted done!');
                }                
            })
        }

        console.error('Converted done!');
    }
});

// Reference: https://stackoverflow.com/a/25012834/6075331

先保存代码为: sitemapInXMLtoText.js,

命令行中用cd命令切换到当前目录,然后依次输入:

1
2
3
npm install fs
npm install request
npm install cheerio

改好key, siteurl, url等值后,就可以在命令行中运行:

1
node sitemapInXMLtoText.js

接下来只需将request调用时的第一个参数改为你的sitemap.xml的网址即可~

最后再到命令行中执行一次:

1
node bing-BatchSumbit2.js

Bing还提供了其他API接口

GetKeywordStats - Bing

1
https://ssl.bing.com/webmaster/api.svc/json/GetKeywordStats?q=dog%20beds&country=be&language=nl-BE&apikey=...

RSS Feed提交:

https://bing.com/webmaster/api.svc/json/SubmitFeed

获取用户验证后的站点信息:

https://ssl.bing.com/webmaster/api.svc/json/GetUserSites

有兴趣的朋友可以继续深入研究哈, 欢迎在评论区留言交流~

FlagCounter被博客园禁用的解决方法

2020年9月8日 05:18

FlagCounter被博客园禁用的解决方法

自从博客园禁用大家常用的ip访问统计插件flag counter后,相信大家多少会有点不太适应。 大白试用了各种类似于flagcounter的工具,发现有两个主要的备选方案,即Tracemyip.orgclustrmaps.com,接下来大白就详细说说具体的用法。

1. Tracemyip.org

首先,注册账号 https://www.tracemyip.org/members/index.php?page=spm_checkout&type=ssub&ntc=1,类型选”Level 1 Free”

极客白小飞p1-Tracemyip.org

然后拖到页面下方,点”Sign up”

接下来,进入页面:

https://s3.tracemyip.org/members/index.php?page=projects_codereg&style=1500~1597899418~14)*2~0F5999*F7FFE6*537899*000000~1*1*0*0*0&rgtype=4684NR-IPIB&urlupdate=

相应短链为: https://d.oo14.com/bNQc

不登录时,得到的是:

极客白小飞p2-Tracemyip.org

填完信息时,会要求填email,然后登陆即可~

代码类型选用: HTML Code - Compatibility Mode 1

由于我的博客园博客是www.cnblogs.com/enjoy233,所以相应地url需要填: www.cnblogs.com/enjoy233.

极客白小飞p3-Tracemyip.org

得到代码后,贴到博客园设置后台的博客侧边栏公告中:

比如,我的是:

1
2
3
4
5
6
7
<div style="line-height:16px;text-align:center;">
    <a title="Joomla hits ip traffic statistics"
        href="https://www.tracemyip.org/tools/website-visitors-counter-traffic-tracker-statistics/">
        <img src="//s3.tracemyip.org/tracker/1500~1597898865~14*2~0F5999*F7FFE6*537899*000000~1*1*0*1*1/4684NR-IPIB/54026/11/njsUrl/"
            alt="Joomla hits ip traffic statistics" style="border:0px;">
    </a>
</div>

效果图如下:

极客白小飞p4-Tracemyip.org


2. clustrmaps.com

注册账号: https://clustrmaps.com/bl/signup

创建Widget: https://clustrmaps.com/add

极客白小飞p1-clustrmaps

填入自己的博客园博客地址,比如我的是:

https://www.cnblogs.com/enjoy233/

然后点”next”按钮即可得到代码:

极客白小飞p2-clustrmaps

1
2
<a href="https://clustrmaps.com/site/1bcz5" title="Visit tracker"><img 
src="//www.clustrmaps.com/map_v2.png?d=Z5dh85VLcC8BLff99dwZ0SHJUomU2T_XMP-WbIcG2wY&cl=ffffff"></a>

接下来,将红线框出部分的代码贴到博客园后台的博客侧边栏公告中:

极客白小飞p3-clustrmaps

保存代码,就可以看到效果了~

极客白小飞p4-clustrmaps

接下来,二选一即可,大功告成~

在GitLab pages上快速搭建Jekyll博客

2020年8月30日 02:23

在GitLab pages上快速搭建Jekyll博客

前一段时间将我的Jekyll静态博客从github pages镜像部署到了 zeit.co(现vercel)上了一份,最近偶然发现gitlab pages也不错,百度也会正常抓取,于是动手倒腾,将github pages快速迁移Jekyll博客到gitlab pages,中途遇到了不少坑,管他呢,一把刷。

于是有了下文的 从github pages快速迁移Jekyll博客到gitlab pages的完全指南。


Step1: 导入已有项目

登陆gitlab.com,点击New Project,

screenshot1

点击Import projects,

image.png

比如,我的jekyll blog对应的代码仓库是: https://github.com/yanglr/yanglr.github.io,

由于我绑定了github账号yanglr,系统会将我在github下所有的分支都load进来,接下来,我选择yanglr/yanglr.github.io,这个仓库,当然Import进来之前要记得改名为: gitlab-username.gitlab.io, 接下来等待导入完成即可。

或点击Repo by URL,

image.png


Step2: 为gitlab生成ssh密钥

1
2
3
4
5
6
7
bravo@localhost MINGW64 /d/coding/Gitlab.com

$ cd ~/.ssh

bravo@localhost MINGW64 ~

$ ssh-keygen -t rsa -b 2048 -C "email@example.com"

然后复制id_rsa.pub的内容,贴到gitlab profile的 settings -> ssh key中,具体入口为: https://gitlab.com/profile/keys

image.png

如果怕github的密钥文件与gitlab的弄混,可以在执行ssh-keygen -t rsa -b 2048 -C ““时,文件名取为: id_rsa_gitlab

然后到 .ssh 文件夹下创建一个文件名为config的文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
# github
    Host github.com
    Hostname github.com
    PreferredAuthentications publickey
    IdentityFile ~/.ssh/id_rsa

# gitlab
    Host gitlab.com
    Hostname gitlab.com
    User legege007
    PreferredAuthentications publickey
    IdentityFile ~/.ssh/id_rsa_gitlab


Step3: Clone代码

设置好ssh key后,我们就可以克隆代码到本地了,命令如下:

1
git clone https://gitlab.com/legege007/legege007.gitlab.io.git

git clone非常慢的解决方法:

在自己的PC上git clone太慢了,clone地址换https的速度还可以~

有能力科学地上网的小伙伴可以想办法解决。

还可以在git clone时,去掉末尾的.git


Step4: 创建 CI/CD配置文件

这部分可以直接参考gitlab官方给的 jekyll 示例 https://gitlab.com/pages/jekyll 来做。

具体做法是,在本地克隆下来的代码仓库的根目录中创建文件 .gitlab.ci.yml, 然后贴入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
image: ruby:latest

variables:
  JEKYLL_ENV: production
  LC_ALL: C.UTF-8

before_script:
  - gem install bundler
  - bundle install

test:
  stage: test
  script:
  - bundle exec jekyll build -d test
  artifacts:
    paths:
    - test
  except:
  - master

pages:
  stage: deploy
  script:
  - bundle exec jekyll build -d public
  artifacts:
    paths:
    - public
  only:
  - master

来源: https://gitlab.com/pages/jekyll/-/blob/master/.gitlab-ci.yml

同理,如果你的静态博客选用的框架是hexo, 请参考项目https://gitlab.com/pages/hexo 下的CI/CD配置文件https://gitlab.com/pages/hexo/-/blob/master/.gitlab-ci.yml,gitbook 项目请用 https://gitlab.com/pages/gitbook/-/blob/master/.gitlab-ci.yml

如果想对gitlab CI/CD有更多的了解,请参看:

https://gitlab.com/help/ci/quick_start/README


Step5: 等待build,2到3分钟build完成后即可访问

点击页面左侧菜单中的 CI/CD,

image5

大概2到3分钟build完成后即可访问,pipelines中会出现一个状态为 passed 的记录,这时就可以访问你的静态博客(pages)了,访问地址是:legege007.gitlab.io,由于gitlab是为gitlab.io默认提供ssl证书的,所有此时也可以访问https版本: https://legege007.gitlab.io


Step6: 绑定域名

现在,我们来绑定自己的域名。

回到项目 刚才的代码仓库,比如我的是: https://gitlab.com/legege007/legege007.gitlab.io,点击左侧 Settings -> pages,会发现右上角有个New Domain,

image6

比如,我想绑定到: yanglr.github.io,填进去,点”Create New Domain”即可。

image7

接下来会看到以下界面:

image8

此时,需要在域名管理网站(比如,我在腾讯云上买的域名,我需要登陆 https://console.cloud.tencent.com/cns),然后在里面先后分别添加一条类型为TXT和一条类型为CNAME的记录。

主机记录 记录类型 线路类型 记录值 MX优先级 TTL(秒) 最后操作时间 操作
_gitlab-pages-verification-code.web TXT 默认 gitlab-pages-verification-code=319ec4807facc9d92c6c62ba4efd1ccf        
blog CNAME 默认 legege007.gitlab.io. - 600 2020-08-30 11:03:33 修改暂停删除

TXT那一条加完之后,可以点击 Verify Status 右侧的 红色按钮旁边的 ↶ ↺ ⟲ 按钮验证网站所有权,该按钮会变成绿色,接下来点击页面下方的”Save Changes”。


Step7: 坐等自动下发SSL证书

最后就是等gitlab自动下发ssl证书。

大概再需要等6分钟左右,页面上的Domain会从http版的 http://blog.yanglr.github.io 变为: https://blog.yanglr.github.io,表明SSL证书已成功获取。

至此,gitlab pages构建完毕。如有疑问,欢迎在评论区提问交流~

长期主义者什么意思?真正的高手都是长期主义

2020年10月13日 18:15

嗯,什么是真正的长期主义?你是否想过这个问题,是坚持一件工作几十年如一日的不动摇,还是死磕一项技能,不管如何惨淡都不放弃,又或者是坚持不懈的努力,脚踏实地,不搞投机。

长期主义就是长周期加坚持吗?如果真是如此,这些标准,我们身边的老好人全部满足,那为什么他们却过如此惨淡?

大家好,我是大白哥。今天我想和大家聊聊什么是真正的长期主义,什么叫真正的长期主义?

长期主义误区一:只盯着长远的目标,不计较当下的得失

极客玩家大白-长期主义误区一

大多数人的第一个误区就是盯着长远的目标,不计较当下的得失。

然而,真正的长期主义是注重短期的,是需要快速迭代,小步快跑。因为外部的环境和你遭遇的问题在不断变化,任何目标都要根据环境的变化而调整。否则时代在变,你不变只会倒在浩荡大势的车轮下被碾压的粉碎。

这里还要引申到一个重要的理念,就是短周期。正确认识长期主义,你就能发现所有的长周期都是由多个短周期组成的。这些短周期的成败,决定了长周期的结果。 譬如我,2018年开始坚持做自媒体,和我一起开始做人很多,向我请教过的人也不少,但99%的人都放弃,不少人都说我没有你这样的天赋,我努力了很久都没有结果。

当一个时间周期达不到时,大多数人都会选择放弃。而这些种种失败的背后,问题其实并不出在天赋或者其他方面,而是大多数人并没有重视短周期,眼睛只是盯着长周期死磕。 而我却恰恰相反,并不像大多数人一样学着写好一篇又一篇文章,而是从写好一个句子开始,到写好一个标题,再从写好一个开头开始,接着到写好一个段落,最后才到写好一篇文章。 很多人一开始就盯着我要写好一篇文章,写了10篇,100篇。 和其他人对比起来深感差距很大,就放弃了。一开始你的方向和目标就错了,结果自然是错的。

长期主义误区二:一味的相信坚持的力量撞破南墙都不回头

长期主义的第二个误区是一味的相信坚持的力量撞破南墙都不回头。

极客玩家大白-长期主义误区二

坚持是好事,但过往的视频里,我不止一次的提到概率论,按照公式来说就是:足够尝试的次数加一定的概率,再小的偶然都会出现必然的结果。

极客玩家大白-概率论

我们常说时间会给我们答案,就是这个原理,很多时候就是如此。 当我们活得足够惨,做的选择足够多时,大数法则就会发生作用。到那个时候我们的命运就不再取决于一到两次的选择,而是整个系统。运气就不再是虚无缥缈的东西。

但问题来了,之所以坚持的原因是因为很多正确的选择,只有把时间拉长才能看到结果,而不是拉长时间,错误的选择也能开出奇迹的花朵。你首先要清楚你的抉择和方向是正确的,然后再坚持,在长时间的周期里才能将偶然变成必然,否则成功率0%的事情在10年里坚持1万字,结果还是0。

长期主义误区三:不清楚长期有多长

长期主义的第三个误区就是:不清楚长期有多长。

极客玩家大白-长期主义误区三

我们嘴里总是说说长期,但你是否想过长期到底有多长,是5年、10年还是一辈子?这些答案都是错误的。长期主义是在符合一件事物的周期曲线里去努力,而不是盲目的去坚持。

譬如,梁宁老师之前提到过一个观点,有一对双胞胎在2010年一起大学毕业,一个加入了腾讯,一个进入了报社。7年之后,去腾讯的那位已经是年薪百万,而且满街都是挖他的猎头,投资人也在挖他,只要出来创业就给钱。去报社的那位,因为报社沉沦,他曾经寄托理想的整个产业都没了,一切都需要重来。

同样在一个长周期里,为什么两者的命运如此不同,答案其实就在周期曲线里,任何事物都有萌芽期、成长期、巅峰期、衰落期和终结期,好比生死轮回。

极客玩家大白-长期主义的周期曲线

因此真正的长期主义是识别周期曲线的变化。然后像傻子一样的努力,而不是用努力去博不确定的未来,知道这点很重要。

长期主义总结

最后帮我们做一个总结。

第一,真正的长期主义不是只注重长周期,而是既注重长期,更注重短期,快速迭代,小步快跑。

第二,长期主义不等于坚持,你要选择正确的方向或者确定这个方向存在概率,然后把时间拉长。然后尝试才会有好结果,否则一定是事与愿违的。

第三,要清楚长期主义的长期有多长,根据自己选择的方向,识别未来的走向,不要与趋势作对。

以上就是我今天的分享,更多精彩内容,大家可以关注公号“大白技术控”,我们下期再见。

pagic零基础入门教程

2020年10月9日 04:15

pagic零基础入门教程

刚接触到Deno不久,据说有位TypeScript大佬基于 Deno 和 React 做出了一款极简的静态网站生成框架 Pagic,大白猜测取名自 Page static,然后缩写为pagic

接下来,大白来分享一下该框架的玩法,给出从零到一的Pagic入门教程,本文以 Windows 10 为例来讲解。

安装Deno

首先,我们需要安装好 Deno。

在Windows下安装Deno,有两种方式

  • git bash中用curl安装

具体命令是:

1
curl -fsSL https://x.deno.js.cn/install.sh | sh -s v1.0.0
  • 在PowerShell中使用chocolate包管理器安装

首先,你需要使用powershell安装好chocolate,具体操作如下:

以管理员权限打开powershell,然后输入以下命令:

1
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))

装好chocolate后,可以直接在PowerShell中以管理员权限运行如下命令:

1
choco install deno

安装pagic

deno安装好之后,下面就可以接着安装pagic了。

打开git bash,输入以下命令可以安装最新版的 pagic:

1
deno install --unstable --allow-read --allow-write --allow-net --name=pagic https://deno.land/x/pagic/mod.ts

处理常见的 Error/Exception

Error 1: error sending request for url … error trying to connect: received corrupt message

执行完刚刚的 deno install ...命令后,出现如下Error:

1
error: error sending request for url (https://deno.land/std/path/mod.ts): error trying to connect: received corrupt message

分析可知:

这个Error是自己电脑上https的证书对 deno 无效引起的。查资料可知 开源项目 mkcert 可完美解决该问题,需要在PowerShell中执行如下命令:

1
choco install mkcert

接下来你还可能遇到下一个Error。

Error 2: error sending request for url … error trying to connect: invalid dnsname

分析可知,这个Error 是因为 deno 引用了github仓库release下的文件导致的。该Error的详细信息如下:

1
error: error sending request for url (https://deno.land/std/examples/curl.ts): error trying to connect: invalid dnsname

其解决方法是:

在 hosts 文件里面添加如下代码:

1
199.232.68.133 raw.githubusercontent.com

如果你的电脑是 Windows 系统,这个文件位置在 C:\Windows\System32\drivers\etc 目录。

MacOS 系统在 /etc/hosts,

设置完hosts之后,ipconfig /flushdns 刷新dns缓存。

问题解决好后,我们继续下一步~

初始化 pagic 项目

要使用 pagic 构建静态网站,则该项目至少需要包含一个 pagic.config.ts 配置文件和一个 md/tsx 页面文件:

site/
├── pagic.config.ts
└── README.md

当然,pagic.config.ts 一开始可以只导出一个空对象:

1
export default {};

README.md 可以是一个简单的 Markdown 文件:

1
# Hi, so cool.

你可以运行以下命令一次性创建出上面的 site 项目:

1
mkdir site && cd site && echo "export default {};" > pagic.config.ts && echo "# Hi, so cool." > README.md

运行 pagic 命令

接下来,我们就可以在项目中使用 pagic 命令了。它的基本用法如下:

1
2
3
4
5
# 构建静态网站
pagic build [options]
# --watch   监听文件变动以重新构建
# --serve   启动本地服务,预览静态网站
# --port    指定本地服务的端口号

打开CMD命令行,切换到 site 目录下,运行以下代码:

1
pagic build --watch --serve

极客玩家大白-image1

咦,pagic命令无法识别呀,原来需要手动将 pagic 加到 系统变量的 PATH 中。

使pagic命令可以在任意目录下的CMD命令行中调用

要在 CMD命令行的任何地方可以使用 pagic,只需把%HOMEPATH%\.deno\bin加到PATH中即可,具体操作看下图:

极客玩家大白-系统变量PATH

此时,再输入同样的命令,就能起作用了:

极客玩家大白-系统变量

打开git bash,切换到 site 目录下,运行以下代码:

1
pagic build --watch --serve

会出现:

1
bash: pgic: command not found

极客玩家大白

使pagic命令可以在任意目录下的git bash中调用

要让 git bash在任何文件目录可以使用 pagic,只需为其设置好alias,并放进 .bashrc文件即可,具体步骤如下:

  • 在个人用户目录 (~)下创建 .bashrc 文件
1
2
cd ~
touch .bashrc


  • .bashrc 文件中添加alias别名:

pagic.cmd的默认安装目录为: /C/Users/{username}/.deno/bin/pagic.cmd.

这里需要把{username}替换为自己的用户名。

然后输入如下命令:

1
notepad .bashrc

加入如下这行即可:

1
alias pagic='winpty /C/Users/{username}/.deno/bin/pagic.cmd'

关闭git bash,然后重新打开即可执行pagic命令了。要想在本地运行pagic项目,只需执行:

1
pagic build --serve

极客玩家大白-系统变量

当然还需要注意一点,build时需要关闭科学上网工具~

然后用浏览器打开 http://127.0.0.1:8000/ ,看看是不是显示出 Hi, so cool. 了呢?

极客玩家大白-静态站点界面

注意,构建结果在 dist 目录中(这里隐藏了一些次要的文件):

site/
|── dist    # 构建结果目录
|   └── index.html
├── pagic.config.ts
└── README.md

一般的 Markdown 文件会被构建为同名的 HTML 文件,但是 README.md 被构建为了 index.html,这是一种人性化的处理,方便同时在 GitHub 中和静态网站中展示首页的内容。

Pagic 只关心 include 指定的 README.md 文件和 docs 目录(放.md文件夹的地方)。 include 中允许配置 glob 格式的字符串,如 docs/*/.md 就只会匹配到 docs 目录下的所有 md 文件。

大白推荐的做法是:

将文件pagic.config.ts 的内容修改为:

1
2
3
export default {
    srcDir: 'docs'
};

然后,把写好的markdown都放到docs文件夹中,这样build后就可以正常访问markdown页面所对应的html了。

使用pagic构建的示例网站

如何在dotnet,git,winget等命令行中使用自动完成功能

2020年10月8日 05:56

转载自 Scott Hanselman.

多年前,.NET core增加了 Powershell或bash中的.NET Core CLI的命令行Tab自动完成 的功能,但很少有人花时间进行设置。

我喜欢设置 并使我的Prompt/命令行/ shell /终端等的体验尽可能有用(和Pretty)。在Windows上,你有很多命令行Shell 可供选择!

请记住,这些是SHELLs,而不是终端或其他控制台。你可以在 Windows Terminal中运行所有这些。

  • 经典cmd.exe命令提示符(伪DOS!) - 仍然很有用,Clink 可以使其更加bash-y,而无需进行全面的bash操作。
  • 来自Malcolm Smith的 Yori
  • Starship - 稍后会有更多介绍,这有点独特
  • Windows PowerShell - 经典,因为…
  • PowerShell 7已经发布并且可以在任何地方运行,包括ARM机器!

我倾向于将PowerShell 7(以前称为PowerShell Core)用作主提示符,因为它是跨OS的提示符。我可以在Windows和Linux上使用相同的提示,相同的功能,所有内容。

极客玩家大白-WindowsTerminal

但是命令行自动补全功能带给我最大的乐趣!

  • git ch <TAB> -> git checkout st <TAB> -> git checkout暂存
  • dotnet bu <TAB> -> dotnet构建
  • dotnet –list-s <TAB> -> dotnet –list-sdks
  • <TAB>中的winget -> winget安装 -> winget安装WinDi <TAB> -> winget安装WinDirStat

一旦你成功地完成了标签操作,就可以轻松获得荣耀。使用PowerShell及其表亲,可以通过Register-ArgumentCompleter使其成为可能。这是dotnet CLI的外观。

1
2
3
4
5
6
7
# PowerShell parameter completion shim for the dotnet CLI
Register-ArgumentCompleter -Native -CommandName dotnet -ScriptBlock {
    param($commandName, $wordToComplete, $cursorPosition)
        dotnet complete --position $cursorPosition "$wordToComplete" | ForEach-Object {
           [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
        }
}

看起来很多,但是唯一重要的是,当它看到命令“dotnet”和一些部分文本并且用户按下TAB时,它将调用cursorPosition和wordToComplete并传递给“dotnet complete”。

注意:如果你了解这是如何工作的,则可以轻松地为一直使用的实用程序制作自己的Argument Completer!你可以为使用你的实用程序的工作人员制作它们!

你从未真正看到过对“dotnet complete”的调用。你只是看到自己输入dotnet bui <TAB>并获得一系列选项即可浏览!

这是后台发生的事情:

1
2
3
4
> dotnet complete --position 3 bui 
build 
build-server 
msbuild

你可以将它们添加到你的$profile中。通常,我在命令行上运行’notepad $profile’,它将在正确的位置自动创建正确的文件。

这是一个超级强大的模式!你可以使用 PoshGit在PowerShell中的Git中 以及在WinGet中获得自动完成功能

你已添加到PowerShell配置文件的一些更晦涩的自动完成功能是什么?

站长之家 seo查询

2020年9月30日 18:21

极客玩家大白-站长之家

作为一枚中文站点的站长,站长之家的SEO工具我们是必须熟悉并掌握的。今天大白进行了全面的整理,分享给大家~

Chinaz站长后台工具

网站管理 - 站长工具

http://my.chinaz.com/ChinazToolSeo/WebsiteManage/Index

极客玩家大白-站长之家-网站管理页

需要注意的是,免费账号最多只能提交一个网站。另外,如果你的站点是https的,在这里的一个站长验证是无法通过的,不过不影响其他功能的使用。

其中的免费功能有如下这些:

SEO监控

域名安全监控

工具中心 - 站长工具

http://my.chinaz.com/ToolMember/Member/Account

ALEXA中心 - 网站管理 - 站长工具

https://alexa.chinaz.com/usercenter/

极客玩家大白-站长之家-Alexa用户中心

日常实用工具 - 站长工具

http://tool.chinaz.com/map.aspx

SEO查询

优化辅助

世界排名

权重查询

数据内参

百度关键词

360关键词

指数查询

域名备案

已备案域名

备案账号安全

IP 查询

同IP网站

域名

网站监控

安全状态

网站状态

网页相关

网页信息抓取

网站链接

反链/死链

短链

友情链接查询结果: http://link.chinaz.com/yanglr.github.io

必应API接口node.js版

2020年9月15日 18:21

近期,在研究百度、必应、API等的url提交API时,发现有用Go语言做工具的大佬的分享 利用 API 自动向搜索引擎提交网址(Go语言版) - pyList

其中提到bing API提交方法,并给出了Go语言代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func Bing() {
    sUrl := "https://ssl.bing.com/webmaster/api.svc/json/SubmitUrl?apikey=xxxxxxxx"
    buf := bytes.NewBufferString(`{
"siteUrl":"https://pylist.com",
"url":"https://pylist.com/t/1581940902"
}`)
    req, err := http.NewRequest("POST", sUrl, buf)
    if err != nil {
        return
    }
    req.Header.Set("Content-Type", "application/json; charset=utf-8")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return
    }
    defer resp.Body.Close()
}

可以保存为: bing-push.go, 然后在本地执行哈~

而相比于go语言,我本人对node.js更熟悉一点~

必应API接口-单条提交

1
2
3
4
5
6
7
8
9
10
11
12
13
var request = require('request');
var options = {
    uri: 'https://ssl.bing.com/webmaster/api.svc/json/SubmitUrl?apikey=' + 'xxx', /* xxx需替换为你的key */
    method: 'POST',
    json: {
        "siteUrl": "http://yanglr.github.io", /* 替换为你的站点,并且在Bing站长平台中验证过权限 */
        "url": "http://yanglr.github.io/link.html"   /* 替换为你需要推送的url */
    }
};

request(options, function (error, response, body) {
    console.log("Bing response: ", body)
});

登录必应站长后台https://www.bing.com/webmasters,点右上角的设置按钮(齿轮⚙),找到你的key:

Step 1:

大奔SEO_极客玩家大白

Step 2:

大奔SEO_极客玩家大白

然后将上述代码中的xxx替换为你的key。

先保存文件为: bing-SingleSumbit.js,

然后在当前目录下打开命令行,输入 npm install request,

接下来改好key, siteurl, url等值后,就可以在命令行中运行:

1
node bing-SingleSumbit.js

必应API接口-批量提交

批量提交 - 版本1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var request = require('request');
var myJson = {
    "siteUrl": "http://yanglr.github.io",
    "urlList": [
        "http://yanglr.github.io/link.html",
        "http://yanglr.github.io/about.html",
        "http://yanglr.github.io/blog/"
    ]
};
request({
    url: 'https://ssl.bing.com/webmaster/api.svc/json/SubmitUrlbatch?apikey=' + 'xxx', /* xxx需替换为你的key */
    method: "POST",
    json: true,   // <--Very important!!!
    body: myJson
}, function (error, response, body) {
    console.log(body);
});

我记得这里有个跨域的问题, 设置 json: true 即可解决。

先将代码保存为: bing-BatchSumbit.js.

运行方法,同上~

批量提交 - 改进1

在上一版本的基础上可以改进,即:把多条url先按行放进link.txt中,然后读取处理~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var fs = require('fs');
var readline = require('readline');
var path = require('path')

function readFileToArr(fReadName, callback) {
    var arr = [];
    var readObj = readline.createInterface({
        input: fs.createReadStream(fReadName)
    });

    readObj.on('line', function (line) {
        arr.push(line);
    });
    readObj.on('close', function () {
        console.log('readLine close....');
        callback(arr);
    });
}

// var urlsFile = path.resolve(__dirname, 'links.txt').replace(/\\/g, '/');  // For Windows
var urlsFile = path.resolve(__dirname, '..', 'nodejs', 'links.txt'); /* 兼容 Windows/Linux, 这里nodejs为上级文件夹名 */

readFileToArr(urlsFile, function (arr) {
    var request = require('request');
    var myJson = {
        "siteUrl": "http://yanglr.github.io",
        "urlList": arr
    };
    
    request({
        url: 'https://ssl.bing.com/webmaster/api.svc/json/SubmitUrlbatch?apikey=' + 'xxx',
        method: "POST",
        json: true,   // <--Very important!!!
        body: myJson
    }, function (error, response, body) {
        console.log(body);
    });
});

保存文件为: bing-BatchSumbit2.js

命令行中用cd命令切换到当前目录,然后依次输入:

1
2
3
npm install fs
npm install readline
npm install path

改好key, siteurl, url等值后,并在当前目录创建文件links.txt并填入需要推送的多条url,就可以在命令行中运行:

1
node bing-BatchSumbit2.js

批量提交 - 改进2

上一版本的代码中,links.txt的内容是手动添加的,那我们可不可以从sitemap.xml获取并直接转换为.txt供后面使用呢?当然可以,于是另外写了一段node.js代码做这个事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var fs = require('fs');
var request = require('request');
const cheerio = require('cheerio');

request('https://www.geekzl.com/sitemap.xml', function (error, response, html) {
    if (!error && response.statusCode == 200) {
        var $ = cheerio.load(html, {
            xmlMode: true
        });

        textFile = 'myLink.txt';
        fs.open(textFile, 'wx', (err, fd) => {
            if (err) {
                if (err.code === 'EEXIST') {
                    console.error('myfile already exists');
                    
                    fs.unlinkSync(textFile);  // Remove file
                }
            }

        });

        const nodes = $('loc');
        var arr = [];
        
        for (i = 0; i < nodes.length; i++) {
            arr[i] = nodes[i].firstChild.data;

            fs.appendFile(textFile, arr[i] + '\r\n', function (err) {
                if (err) {
                    console.error('One line converted failed.'); // append failed
                } else {
                    // console.error('One line converted done!');
                }                
            })
        }

        console.error('Converted done!');
    }
});

// Reference: https://stackoverflow.com/a/25012834/6075331

先保存代码为: sitemapInXMLtoText.js,

命令行中用cd命令切换到当前目录,然后依次输入:

1
2
3
npm install fs
npm install request
npm install cheerio

改好key, siteurl, url等值后,就可以在命令行中运行:

1
node sitemapInXMLtoText.js

接下来只需将request调用时的第一个参数改为你的sitemap.xml的网址即可~

最后再到命令行中执行一次:

1
node bing-BatchSumbit2.js

Bing还提供了其他API接口

GetKeywordStats - Bing

1
https://ssl.bing.com/webmaster/api.svc/json/GetKeywordStats?q=dog%20beds&country=be&language=nl-BE&apikey=...

RSS Feed提交:

https://bing.com/webmaster/api.svc/json/SubmitFeed

获取用户验证后的站点信息:

https://ssl.bing.com/webmaster/api.svc/json/GetUserSites

有兴趣的朋友可以继续深入研究哈, 欢迎在评论区留言交流~

FlagCounter被博客园禁用的解决方法

2020年9月8日 05:18

FlagCounter被博客园禁用的解决方法

自从博客园禁用大家常用的ip访问统计插件flag counter后,相信大家多少会有点不太适应。 大白试用了各种类似于flagcounter的工具,发现有两个主要的备选方案,即Tracemyip.orgclustrmaps.com,接下来大白就详细说说具体的用法。

1. Tracemyip.org

首先,注册账号 https://www.tracemyip.org/members/index.php?page=spm_checkout&type=ssub&ntc=1,类型选”Level 1 Free”

极客玩家大白p1-Tracemyip.org

然后拖到页面下方,点”Sign up”

接下来,进入页面:

https://s3.tracemyip.org/members/index.php?page=projects_codereg&style=1500~1597899418~14)*2~0F5999*F7FFE6*537899*000000~1*1*0*0*0&rgtype=4684NR-IPIB&urlupdate=

相应短链为: https://d.oo14.com/bNQc

不登录时,得到的是:

极客玩家大白p2-Tracemyip.org

填完信息时,会要求填email,然后登陆即可~

代码类型选用: HTML Code - Compatibility Mode 1

由于我的博客园博客是www.cnblogs.com/enjoy233,所以相应地url需要填: www.cnblogs.com/enjoy233.

极客玩家大白p3-Tracemyip.org

得到代码后,贴到博客园设置后台的博客侧边栏公告中:

比如,我的是:

1
2
3
4
5
6
7
<div style="line-height:16px;text-align:center;">
    <a title="Joomla hits ip traffic statistics"
        href="https://www.tracemyip.org/tools/website-visitors-counter-traffic-tracker-statistics/">
        <img src="//s3.tracemyip.org/tracker/1500~1597898865~14*2~0F5999*F7FFE6*537899*000000~1*1*0*1*1/4684NR-IPIB/54026/11/njsUrl/"
            alt="Joomla hits ip traffic statistics" style="border:0px;">
    </a>
</div>

效果图如下:

极客玩家大白p4-Tracemyip.org


2. clustrmaps.com

注册账号: https://clustrmaps.com/bl/signup

创建Widget: https://clustrmaps.com/add

极客玩家大白p1-clustrmaps

填入自己的博客园博客地址,比如我的是:

https://www.cnblogs.com/enjoy233/

然后点”next”按钮即可得到代码:

极客玩家大白p2-clustrmaps

1
2
<a href="https://clustrmaps.com/site/1bcz5" title="Visit tracker"><img 
src="//www.clustrmaps.com/map_v2.png?d=Z5dh85VLcC8BLff99dwZ0SHJUomU2T_XMP-WbIcG2wY&cl=ffffff"></a>

接下来,将红线框出部分的代码贴到博客园后台的博客侧边栏公告中:

极客玩家大白p3-clustrmaps

保存代码,就可以看到效果了~

极客玩家大白p4-clustrmaps

接下来,二选一即可,大功告成~

国内SEO英雄榜,你知道几个?

2020年8月28日 08:00

by 刘明

名单中列出的大部分都是《百度站长学院VIP大讲堂》学员。

百度站长学院 供稿

昝辉

网名:ZAC

极客玩家大白-seoZac

Email:zanhui@gmail.com(来自本人博客)

描述:SEO行业教父,《SEO实战密码》作者。2000年做自己的第一个个人爱好网站,从此一发不可收拾。2003年辞职,创建中新网络科技Silver Media,全职SEO(搜索引擎优化),虚拟主机等以网络为中心的生意。目前是新加坡发展最快的虚拟主机和服务器提供商。

刘明

网名:seoaqua

极客玩家大白-刘明

QQ/微信:628552(经本人同意)

描述:曾就职于新浪,网易。现任艺龙网SEO总监,SEO渠道的酒店订单连续两年翻翻。研发和架构能力强,善于通过技术手段

(Ruby+Linux+Mysql+Redis+Hadoop+Git)搭建SEO系统(关键词挖掘、友链系统、差异化内容API、SEO自动测试、蜘蛛行为分析行业排名趋势)解决问题。带领团队设计并研发了自动化SEM系统(物料管理、关键词粒度的数据统计、关键词挖掘和自动识别、多维度精准调优),适用于全部搜索引擎,自称强于绝大部分外包公司。对ASO有一定研究和实践。

相关文章(可在站长学院找到):

如何避免大量重复URL被百度收录

如何保证网站长期的SEO健康度

利用百度蜘蛛referer找到报错页面入口

安琦

网名:an7 QQ:54818355(经本人同意) 描述:早年从事外贸seo,现在携程无线营销渠道产品组负责人

申永

网名:大卫

QQ/微信:seoview(经本人同意)

描述:2003年涉足SEO,国内SEO老兵,曾就职于香港大型外贸公司,负责上千站群Google,Yahoo, Bing排名优化。后续先后就职阿里巴巴国际站,中文站流量业务负责人。14年中英文大型B2B,B2C,企业官网,旺铺等网站SEO,ASO,SEM,DSP,VSO,Affiliate,自媒体营销推广经验。

飞鹰正义

QQ:356252296(经本人同意)

描述:百度站长社区超级版主

ID:飞鹰正义,曾有过一段斗志昂扬的红客史擅长

windows下安全攻防,是国内较早的百度seo人,为人低调帅气,现任中国服装网SEO 负责人。

胡金环

网名:神马浮云

QQ:1038442403

描述:专业从事房产网站平台seo,sem工作,原房途网/房探007项目seo总监,现就 职酷客美地从事移动互联网运营推广工作。

胡亮亮

描述:新网迈广告seo总监

柳焕斌

极客玩家大白-柳焕斌Robin

描述:Robin,在线营销专家,资深电商从业者,从事互联网工作9年。2006年发起和联合创建SEO公益分享组织点石互动,2009年加入麦包包,分管在线营销业务,现担任凯络中国电商总经理,负责电商平台布局业务。

SEO网红

齐军瑞

网名:痞子瑞

极客玩家大白-痞子瑞

微信:seoshendu(经本人同意)

描述:《SEO深度解析》作者

陈汝一

QQ:1296429673(经本人同意)

网名:夜息

极客玩家大白_夜息yexi

描述:曾任途牛网seo负责人,SEO顾问服务过多个行业,大中小规模的网站,并且取得了明显的流量增长效果。 在业内具有良好的人脉资源,与国内各大互联网公司相关人士有密切交流。

张闻一

网名:Zero

极客玩家大白_张闻一Zero

描述:曾负责过赶集、美团、知乎等网站的SEO,突出成绩如三个月内使美团的百度SEO流量不止翻倍。在搜索引擎及技术方向,有广泛且较深入的研究。创办SEO前线主营SEO顾问业务,服务于智联招聘、下厨房等网站。且运营行业论坛,将行业竞争最大的词”SEO”的排名做到百度首页。

张国平

作品: sem一家之言

描述:杭州光年信息技术有限公司创始人,著名SEO专家。从个人站长到阿里巴巴SEO负责人,张国平先生凭借自己的优秀SEO经验技术使得众多网站流量得到飞速提升,也因此在业内获得广泛赞誉。他以”实战派”SEO为人所称道,注重数据分析,坚持用数据来指导SEO,提倡”做科学的SEO”。杭州光年自创办以来,已经先后成功的为”易趣”、”携程”、”19楼”、”证券之星”、”阿里巴巴日文站”等知名网站提了SEO咨询服务,并且获得了客户的高度评价。

黄凤华

网名:夫唯

描述:seowhy创始人。本科就读于山东大学,研究生就读于北京师范大学。夫唯先生推崇”循序渐进,基业长青”的SEO运维思想,以SEOWHY为实战案例,历经2年零起步创造了网站品牌,为SEO行业奉献了一个优秀的行业平台和成功案例。 WW

上市公司营销高管

付必鹏

QQ:保密

阿里国际站资深总监,点石互动联合创始人。

孙波

QQ:保密

描述:携程网在线营销副总裁。资深互联网民工,曾就职于阿里巴巴、5173等。

韦护 (微信:whooll)

现京东 市场无线营销 高级经理,(09年~14年负责京东市场流量拓展部 ,业务板块:全站SEO、流量BD、精选商品频道、API合作和流量数据分析等),07年开始网赚、网络营销、seo、建站等内容。

孙健(小健)

QQ:307824429(经本人同意)

描述:前国美、库巴流量产品运营部总监;现任慧聪网流量部总监。

刘斌

QQ:478800361(经本人同意)

网名:BinGo

描述:夫唯学院第五期学员,搜房网平台事业部总监,负责搜房网seo、sem和搜索合 作。

SEO服务提供商

陈超

QQ:10511707(来自本人公开微博)

描述:上海刑天文化传播有限公司创始人,SEO服务提供商。

狼雨

QQ:20985142(经本人同意)

描述:2011年一站成名。狼雨SEO创始人,提供外包服务。

方法(这是真名)

QQ:123119329(经本人同意)

SEO服务提供商,提供SEO顾问、SEO全案外包、SEO项目合作、SEO培训等服务。现任智联招聘高级SEO顾问,负责智联招聘移动端SEO规划实施,以及智联旗下高端招聘平台智联卓聘的PC+移动端全部SEO相关工作。个人博客(http://seofangfa.com)及个人微信订阅号(seofangfa)是一个以分享科学SEO理念为主的平台,也收集整理了很多国内顶级SEO的干货文章,目前每天有1000多个独立访客。

小向

QQ:8943489(经本人同意)

公司:脉速营销 IMAISU

描述:4A广告公司投资,拥有4A策划团队结合知名互联网运营背景,超过8年SEO实战经验,6年SEM操作经验,4年SEO培训经验。服务于旅游、电商、金融、快消、医疗、教育、机械、数码、育儿等多个领域客户,为携程、平安、爱家、众安、好孩子、舒适等100家知名品牌网站提供顾问及优化服务。

郑志平

爱站网创始人,现为爱站网络科技有限公司CEO、搜狐SEO总顾问。

李华东

A5站长网SEO总监,A5首席SEO诊断师。

焦宗宗

网名:焦大 QQ:651207990(经本人同意) 描述:11年开始做seo,先后建立seo培训联盟和seo学堂,目前独立创业,主要承接网 站seo项目合作以及seo培训。

坐馆欢喜哥

微信:wilsonwang21

1、Artand,中国艺术类社交电商门户,Alexa全球排名 39963,UV 12000 / PV 48000 2、Modesens,时尚海淘一站式聚合搜索工具,Alexa全球排名 5485,IP 36000 / PV 115200 3、FiboGroup.com,全球投资金融平台,Alexa全球排名20万位

埋头苦干的兄弟姐妹

刘梦晨

网名:FBSENDER

描述:艺龙在线营销部主程。原凡客库存系统研发工程师。来艺龙开发SEM自动化投放 系统。擅长大型网站SEM投放及优化技术。现负责艺龙营销部全部技术工作及部分SEO业 务。技术强人,效率至上,逻辑思维和业务思维并重。关键词”羽毛球规则”在百度排名 稳定第一。

刘铮

网名:银狐云枫

QQ:42397890(经本人同意)

描述:现任一亩田SEO总监。曾任百度知识类产品SEO全线负责人。赶集SEO总监。马可波罗海外部SEO总监。经历项目:中国供应商 360圈 中国日报 28商机 6间房 PPS搜易 中华英才 九城 中国台湾网 天津网 威客网 西陆网 达芙妮

唐安

网名:安洋

QQ:281826888

5年个人站长seo经验 2010年加入4399到至今

目前主要负责游戏资讯 4399手机游戏媒体网站运营总监

殷晓俊

QQ:190714307(经本人同意)

描述:房多多市场部高级经理 流量负责人 曾就职携程,平安

孙炜亮

QQ:26344665

土豆、乐视SEO负责人,SEO内链体系获得国家专利。用数据做SEO。

胡星宇

网名:老胡

软件开发多年,SEO&SEM多年,现主要从事数据分析,管理电子化与运营管理

吴敏

QQ:131289(经本人同意)

描述:曾就职于搜狐,19楼。现任贝贝网营销合作总监。个人站长出身,SEO顾问过好 多个行业,行业人脉资源广泛,低调华丽。

郭明阳

微信:gmy229

世界工厂网SEO负责人,长期深耕B2B行业

杨妮娜

QQ:371370684(经本人同意)

描述: 现任百度海外搜索产品SEO,ASO负责人。

曾任百度知识搜索产品优化(知道,文库,经验,地图),F团SEO负责人

其他项目:外贸行业,78,工控网

高庆华

QQ:112143235(经本人同意)

铁友网SEO负责人。06年就兼从事企业网站SEO外包服务,2010年开始负责铁友网和携 程汽车票频道,有着大站的5年SEO营销团队组建和SEO内容运营经验,在职期间2014年 因为移动端的行业趋势,我们很快跟进并参加百度无线搜索适配快速迭代上线对应的移 动频道页面,获得百度”奋斗”奖。铁友网初创期,负责过2年的产品运营部门,由于现 阶段职业瓶颈去了一家互联网创业公司专门负责产品运营部门

张赵伟

QQ:1420186160(经本人同意)

描述:现就职蘑菇街,曾花瓣网SEO负责人。

百度首届移动优化大赛专家奖获得者,给花瓣网SEO流量翻了16倍。

黄齐明

QQ:1491993033(经本人同意)

描述:赢商网SEO负责人。曾任职于5U体育(原U体育),妆品网等网站SEO负责人。

李卫国

QQ:294513544(经本人同意)

描述:就职于寻医问药。专业从事4年医疗SEO

小黑

QQ:290282086(经本人同意)

描述:互联网安全技术出身,一时迷于黑客之间,后接触SEO,有幸结识了不少圈内人 士,单页优化流量超过2W。曾就职光年论坛、京东商城。

师旭

QQ:544129967(经本人同意)

描述:就职于世界工厂网。专注于互联网产品设计、中英文搜索引擎优化、B2B网站运 营、企业网站SEM

张洺乾

QQ:744949589(经本人同意)

描述:原到到网(改名为猫途鹰)SEO/SEM负责人,双料选手,一个人能顶俩。平时会 写程序(Ruby语言)来辅助日常工作。

张基

QQ:23853329(经本人同意) 描述:阿里巴巴

张阳

QQ:179782221(经本人同意) 描述:原zhuna.cn SEO负责人,专长于酒店行业SEO

陈明鹏

描述:途牛PC站SEO负责人

吕翠香

QQ:361722397(经本人同意)

09年从事SEO工作,曾任职于赶集网,易车网,目前就职于智联招聘

张雪峰

网名:放肆绅士

QQ:53854854(经本人同意)

描述:穷游网 网络营销经理,SEO、ASO负责人,擅长数据分析及A/B测试,并通过数据驱动带领团队实现每年流量增长一倍及转化率提升,积累了旅游,酒店,教育,美食,电商,招聘,社区等行业的丰富项目经验,曾任Netconcepts耐特康赛SEO项目经理,服务于艾瑞,智联招聘,优购,国航,贝太厨房,爱结,英孚教育等。

章螂

QQ:63224(经本人同意) 描述:天气网(tianqi.com)CEO/SEO。

孙炜亮

原乐视网SEO负责人

王龙

QQ:280026798(经本人同意)

描述:大众养生网SEO负责人。曾就职于世邦机器。

蒋孟奇

QQ:975327365(经本人同意)

酷讯搜索营销主管,擅长SEO、SEM、ASO等相关领域

贪狼

土巴兔SEO负责人

吴嘉阳

QQ:718831725(经本人同意) 描述:Digital marketing and consumer experience focused. Passionate about online marketing and consulting. Seeking to combine digital marketing skills with international background in a leading technology company. Self motivated, good working in team. Proficient knowledge of SEO,PPC Ads(Google Adwords),Display ADs.

王建中

上海刻缘营销创始人;上海蔚为医美副总

袁大吉

QQ:61866257(经本人同意)

描述:39健康网 流量合作seo总监 8年seo经验,曾负责多个b2c ,b2b,金融门户 网站优化,也有丰富的站群流量站优化经验!!

周文军

QQ:252087418(经本人同意)

阿里巴巴

黄达和

网名:小二

QQ:1290505352

描述:99健康网SEO负责人。曾任职于火车网,品牌网,

金鑫

QQ:19516833(经本人同意)

337网

8年海内外搜索营销经验,业内人脉丰富。

吕令建

E-mail: lvlingjian@yeah.net

大家称之为令建,山东泰安人,现居北京。专注于大型网站的SEO流量提升,对十万百 万千万级流量的网站有独特的手法。先后任职于慧聪网,去哪儿,负责整体SEO工作。 不爱露面的SEO工作者。

泡面

描述:本地宝联合创始人,总编辑,seo负责人,多年SEO实战经验。

无天

OurSEO站长,专注企业网站SEO优化、网络推广、品牌营销,挖掘、研究SEO。无天此博客,是一个关注互联网,关注网站运营、搜索优化、营销策划、用户体验的IT博客。

林国平

网名:乱发型

QQ:1935959013(经本人同意)

描述:就职于4399。有4,5年SEO经验

姚磊

QQ:283146795(经本人同意)

网名:大姚

描述:多特软件站seo负责人,曾就职于购酒网,热衷于B2C运营和推广。

杨文军

网名:魔法师

email:yangwenjun61@sina.com(经本人同意)

描述:现就职于考拉FM,负责PC端流量。曾就职于淘房网、云通讯平台、中国网库。擅长SEO、网络营销、品牌推广。

磊子

描述:就职爱奇艺,激动网,视频seo爱好者

王润

QQ:398872331(经本人同意)

描述:什么值得买SEO和数据分析负责人

程堂军

QQ:87621304(经本人同意)

描述:58安居客高级流量经理,负责SEO流量,及其它免费流量。

马如意

网名:如意

QQ:651324810(经本人同意)

描述:曾负责新浪乐居SEO/SEM、百度乐居房产网站SEO工作。

肖洁纯

网名:洁纯

QQ:305624763(经本人同意)

描述:现就职拉手网SE0主管一职,曾任职新东方,负责新东方集团SEO优化。 经历项目:新东方(新东方宝典)、拉手网商家库

苏云田

网名:亦奇

QQ:182185871(经本人同意)

描述:500彩票seo负责人。6年seo。曾就职环球资源、亚太传媒集团。

王秀杰

描述:就职于中关村在线,主要负责中关村在线论坛、下载等相关业务。

贺涛

网名:wnipai

QQ:345311821(经本人同意)

描述:2013年加入百姓网,14年开始以数据分析师的角度开始接触SEO,现在负责百姓网SEO的运营。

张化祥

网名:SEO祥子

QQ:236213140(经本人同意)

描述:曾任职于Netconcepts、悠易互通广告有限公司,现负责新浪乐居SEO工作。主 要技能SEO,熟悉SEM、DSP、广点通、社会化媒体等营销推广。

Stevezhou

QQ:1185521130(由其好基友推荐)

没有天份 还剩天真。

技术派SEO;规划驴妈妈产品端总体SEO布局与运营,成绩显赫;构建SEO管理系统(全 站TKD管理、内链、友链等)以及工具系统(日志、需求词库、排名等)、其中研发的 排名工具速度和实用性值得称赞。怀着一份情怀,寻找方向的家伙,希望能遇见志同道 合的小伙伴。博客地址stevezhou.info。工具地址:tool.stevezhou.info(鉴于 保密协议,日志数据已下线)

王敏

网名:王威廉 QQ:373570383(经本人同意) 描述:就职好未来,负责K12站群SEO/ASO工作。

道哥

微信:seodug(经本人同意) QQ:59037148(经本人同意) 《SEO深度解析》第二版联合作者

赵彦刚

微信:zygseo(经本人同意) 描述:原中公网校SEO负责人,个人站长出身,懂点技术。个人博客(seojishu.com)曾在业内 被转载数十万次,微信订阅号(seozyg)。强调以数据为驱动,以结果为导向。

吴秀龙

QQ:1009926370(经本人同意)

微信:longhua0612(经本人同意)

描述:负责中公教育集团SEO、优就业SEO研究院院长,专注教育行业SEO研究分析,同时涉足电商、酒店旅游、社交问答等领域。

王保良

QQ:1285294056

微信:happywbl

描述:中国制造交易网 SEO负责人。

刀心

QQ:1136018016

描述: 曾负责寺库secoo.com的SEO。多年个人站长经验,操作过数20万PV的个人站。


最后,本人:

微信628552,注明来意。不收徒。

https://www.linkedin.com/in/thaddeus-liu

Getting good at Leetcode

2021年10月11日 22:02

如何变得擅长LeetCode

Getting good at Leetcode

by Heidi Newton

来源: https://heidi-newton.com/blog/getting-good-at-leetcode

Disclaimer: This post is in no way affiliated with Leetcode, or endorsed by Leetcode. I am just a user/ fan of Leetcode.

Lately, I've had a number of people asking me for advice on how to improve at answering the style of questions found on Leetcode. I'm not an expert in job interviewing—in fact, my experience in it is quite limited. But I think my experiences and improvement with solving Leetcode questions means I can give some useful advice to others. So, for what it's worth, I'm going to share some tips on how I think I've gotten where I have with it.

I started doing Leetcode questions when I wanted to get better at competitive programming, after really enjoying the Google Code Jam (Now called Kick Start) but never doing well beyond the 1st round. Fast forward to a few months later, and I've now solved almost 600 Leetcode questions, with the ultimate goal of solving them all. I take part in the Leetcode weekly competitions, where I have seen my weekly ranks slowly but surely go up. I went from only solving 1 - 3 questions per week, up to more-often-than-not, solving all 4. I went from getting only 5 participation coins per week, to getting 50 most weeks for being in the top 200. Hopefully soon I'll be in the top 100.

So with that, here we go. My 20 best tips that I hope will help you achieve the same!

1. Know your motivations

Why do you want to "get good at Leetcode"? Do you want to get your dream job? Win programming contests? Learn more about algorithms? Without a good motivation, you may struggle. If your only motivation is to get your dream job, then you might also struggle to stay motivated, especially with the "risk" of not getting the job. If you don't enjoy solving the questions, it might not be worth doing the advanced ones. There are plenty of good companies out there where you won't need to be able to solve medium-level and hard-level Leetcode questions to get into them.

So, think about what motivates you, and keep that motivation in mind!

2. Set realistic goals for yourself

Multiple times, I've seen people ask how many questions they should solve in a day. This is the wrong question to ask though. You shouldn't have a number-of-questions-per-day target. Instead, you should decide how much time you are willing and able to put towards your learning, and then make it your goal to use that time as productively as you can, aiming to improve. You can only do your best, and you should be aiming to do your best, so comparing to other people won't be helpful here.

Aim to divide your time between learning new skills, mastering old skills and general problem solving (tackling questions without initially knowing what skills you'll be applying).

3. If you don't have a CS degree, do an online data structures and algorithms course

If it wasn't for the first year data structures and algorithms course I did back when I was in university, I don't think I would have known where to start. Getting started can be daunting, and knowing what exactly to focus on can be even more so. As a rough guide, you'll need a course that takes you over key ideas such as Arrays, Sorting, Searching, Linked Lists, Trees, Graphs, Stacks, Heaps, Queues, Hashing (maps, sets, and dictionaries), Big-O notation, and complexity analysis (cost). By the end of it, you should know the use cases for each data structure, and be able to justify the reasons in terms of cost.

A good online course will have assignments and quizzes that'll help you know whether or not you're on track and picking up the most important ideas. This will then help you as you move towards self-directed learning on more advanced concepts.

There are also many good resources on the more basic programming skills. Find one that suits your style and language of choice. Make sure you can design programs with loops and conditionals (if statements) with ease, as this is central to algorithm design.

4. Find some good resources for learning more advanced algorithms

It really does help to have a good resource to look at when needed. I use the CLRS algorithms book. I'd highly recommend it, either go for the e-book version on Amazon or look for it in the library (if you're a university student with access to a library) if the cost is a concern. The Leetcode Explore modules are fantastic too. Geeks For Geeks also has some useful content, although the writing quality varies a lot and the lack of proofreading on some of the articles can make it painful to read. If you can ignore that, great, if not, then you might need to find another resource. Wikipedia also has some good articles, although quality and clarity varies a lot.

5. Learn techniques, not solutions

I've seen a few people on the Leetcode forums and beyond asking how many Leetcode questions they'd need to have done to have a good chance of seeing all the questions they'll get in their interview, and it amazes me that anybody thinks this is a good idea. Leetcode itself doesn't help the cause by providing lists of questions sorted by company (for subscribers).

If in an interview you've already seen the question, it'd be dishonest to pretend you hadn't. You'd also have to be pretty good at faking the “solving a novel problem” thought process your interviewer is looking for.

Instead, you should be aiming to do enough questions to learn about the common data structures, algorithms, and techniques that you're likely to need to use. Your goal is to get yourself to a point where if you're given a problem you've never seen before, you can work through it and decide which data structures, algorithms, and techniques you could apply to solve it. It will take a lot of practice to get to this point, and sometimes there'll be a question where it takes you a few minutes to even know where to start. But the more you learn and practice, the better you will become.

6. Learn the fundamental data structures (Basic)

These next 3 points are concerned with specific concepts you'll need to learn. I've split them into 3 general categories, based on difficulty and how often they tend to come up. I'd recommend mastering all in one category before moving up to the next. The lists aren't exhaustive, and the order isn't absolute, but this is roughly the order I worked on learning and mastering concepts.

If you have a lot of trouble with even the simplest questions in the fundamental data structures section (some are hard, I'm not saying you have to find them all easy!), then you might need to find an introduction to data structures and algorithms course to work through. Most of these topics have a Leetcode Explore module, I highly recommend using them. After that, find more problems by searching the problem list by the relevant tag. Focus mostly on easy-level questions for now.

  • Arrays and Strings: Leetcode's module for Arrays and Strings is where you should start. This is a fundamental topic, so practice lots until you're really confident.
  • Linked Lists: Again, check out the module for Linked Lists. Don't worry about the solution guides that are subscribers only—you don't need them. Check out the discuss forum instead, click on posts until you find a good one. Linked lists can be confusing at first, but they're easy as once you get your head around the idea of the next pointers.
  • Trees: Focus on the N-ary trees module and then the Binary Trees module. Don't worry about Binary Search Trees yet, you can look at those a bit later. For some reason, the information documents about traversals are subscribers only. Don't worry about this though, there are plenty of great free resources around, such as this overview and these widgets that get you to click on nodes in the correct order. You'll find lots more by googling.
  • Hashing: This is dictionaries/maps, and sets, and is covered under the Hash Tables module. This is one of the most useful topics you'll learn, so be sure to get lots of practice in, as most challenging questions use these data structures as part of the solution. Don't worry about the few bits that are subscribers only.
  • Stacks, queues, and heaps (priority queues): These are also fairly versatile, coming up in a lot of problems. Stacks and Queues have a module, although heaps/ priority queues don't yet. Have a look on Geeks For Geeks for some good articles on heaps, and then find some questions on Leetcode that are solved with a heap. Check if your programming language of choice has a built-in heap type, it will be called either a heap or a priority queue.
  • Two pointer technique: If you didn't do this with arrays, you should do it now. It comes up a lot.

Make sure you know complexity costs of these basic data structures, for example, that finding whether or not something is in a linked list is O(n), but finding whether or not its in a dictionary is O(1). You should understand the costs inside and out, knowing exactly why they are what they are. Simply memorizing the costs won't help you much.

7. Learn the more advanced data structures and some famous algorithms (Intermediate)

There are some algorithms and a few more data structures that will come up over and over again. It is well worth putting time into studying each of these, much like with the fundamental data structures. Some of them will be in the easy category and some in medium. The hard ones tend to combine multiple concepts.

  • Recursion: This alternative to iteration is helpful for solving some problems, and once you're used to it, is often quicker to write than iteration. Leetcode has a module for recursion. Just do this module, don't worry too much about recursion 2 yet, there are more important concepts for you to learn first.
  • Graphs and graph algorithms: Graph algorithms come up everywhere. I've heard that in the interview processes of top companies, getting at least one graph question is inevitable. The main things they are looking for is that you know the 3 main ways of representing a graph: linked, adjacency list, and adjacency matrix, choosing the best one for a given situation, and then running algorithms over it such as breadth-first search, depth-first search, and topological sort. Often graph questions aren't explicitly listed as such—you'll be given some data as an input that you need to convert into a valid graph format, and then run the appropriate algorithm over it. You must get really good at recognizing graph questions instantly. Be aware too that a 2D array can represent a special kind of graph where each cell represents a node linked to its neighboring cells. Often these are breadth-first search questions. You should have already seen some of this in stacks and queues, and trees. But be sure you're super confident with graphs. You should be able to BFS and DFS both iteratively and recursively, and with your eyes closed (okay, maybe not that extreme, but you should be really confident with them!).
  • Union find: This algorithm can initially seem scary, but it's not. It's useful for finding the connected components of graphs and solving a number of other problems. Have a look on Geeks for Geeks to get started or the CLRS textbook if you own a copy. There is no Leetcode module on it yet, but there are lots of relevant questions. Learn the simple optimizations, in particular path compression, as while they sound advanced, they're actually really simple and lead to huge efficiency gains.
  • Binary search: People tend to get quite bothered by this algorithm and its edge cases and resort to guessing and checking loop and conditional conditions. I'd recommend doing the binary search module, but also trying to build intuition around the algorithm. Be cautious on relying too heavily on the templates given in the module, they are good to get started, but trying to memorize the templates will only lead to edge-case bugs that you will struggle to debug. It helps to know whether your “middle index” formula is getting the lower-middle or upper-middle (when there's an even sized list) and then whether or not your low and high moving will definitely converge instead of infinite looping. Sometimes making it converge is as simple as using the upper-middle instead of lower-middle, but it depends on how you're changing low and high. Overall, you must build an intuition rather than trying to memorize the algorithm. I might write a blog on this at some point.
  • Binary search trees: These, along with generic binary trees (i.e. not sorted tree), seem to come up almost every week in the weekly competition. Being able to quickly write a recursive function to traverse the tree and get the information you need is possibly the single biggest thing you can do to improve your competition performance. They tend to be medium-level questions, but if you know what you're doing, they are easy to do in under 5 minutes! Start with the Binary Search Tree module, and then look for more questions on trees. Make sure to do lots of medium-level questions.
  • Tries and advanced string algorithms: Leetcode has a module on Tries for you to work through. As for other string algorithms, Google them or look in the CLRS algorithms book.
  • Interval trees: These are probably the most advanced data structure you'll see on a semi-regular basis. I'd recommend trying to code one. Unfortunately, Leetcode doesn't have an Explore module on them.

8. Learn the versatile algorithmic design techniques (Advanced)

This is the stage I'm currently at. It was working on these techniques that has really driven my improved performance in the competitions. They are not easy, and you'll need to work and work and work at them. You'll need to start by practicing questions where you already know which technique you'll be using, and then on ones where you don't. The competitions are useful for that, as you don't get to see the tags until the end.

  • Bit manipulation: This isn't that difficult once you get your head around it, and it's also pretty fun. Leetcode has many questions on bit manipulation, although no module for it at the time of writing. To get started, Google a guide for your programming language of choice, then have a go at some of the questions. Note that while it can be used standalone for most of the easy and medium level questions, it tends to be an optimization technique combined with other concepts in hard-level questions.
  • Backtracking: This is a strategy that many advanced recursive algorithms use, especially when coming up with solutions for NP-Complete problems. Before I learned how to do backtracking correctly, my exponential time algorithms would also use exponential space, with lists and sets getting copied everywhere. I see many hard-level questions that are straightforward with backtracking. Leetcode has a section on it, tucked inside the Recursion 2 module.
  • Divide and conquer: This is another strategy that many advanced recursive algorithms use, in particular, those that run in O(n log(n)) time (quasilinear). Mergesort and Quicksort are the most well-known examples of divide and conquer algorithms. Leetcode has a section on them, also tucked inside the Recursion 2 module.
  • Parsing: Parsing questions are very difficult if you don't know how to approach them, but very fun if you do. One technique is recursive descent, but there are also others such as using an explicit stack. Some good questions are Number of Atoms, Parse Lisp Expression, Parsing a Boolean Expression, and Evaluate Reverse Polish Notation.
  • Dynamic programming: This may be the hardest technique, and unfortunately I'm still looking for a good resource on it. The CLRS algorithms book has some good content on it though, and Leetcode has many awesome dynamic programming questions. Come on Leetcode, add an Explore module for this topic!
  • Memoization: This is an alternative to dynamic programming that instead uses recursion. It tends to be easier to reason about (for most people), so it might save you for the more challenging dynamic programming problems. In saying that, your goal should ultimately be to do as many questions as possible using dynamic programming instead of memoization. Leetcode has on it, tucked inside the Recusion 1 module.

Keep in mind too that there's often more than one approach for a question, and the tags aren't always exhaustive. For example, the Smallest Sufficient Team problem is tagged as two of these techniques, but also has really nice solutions with one of the others. I'm being intentionally vague as I don't want to give spoilers.

9. Be aware of the intractable problems, particularly the NP-Complete ones.

A computer scientist's idea of a practical joke is to give the unsuspecting victim an NP-Complete problem to solve and watch them go around in circles, possibly discovering what they think is a solution and then discovering that it fails in some cases. Okay, maybe we're not that mean. But I have definitely heard of people in interviews trying to come up with an efficient solution for a problem which is NP-Complete, and if they DID find a good solution, they wouldn't be needing the job they're interviewing for (as they'd be rich for it!). I have even heard of companies trying to solve them, not realizing. Or apps that promise to give you the shortest path between multiple stops.

So, know what they are. Wikipedia has some good lists of them. Know what the best way to solve them is⁠—sometimes it's a brute force, but with clever optimizations. Other problems have dynamic programming solutions which are what we call pseudo-polynomial. The knapsack problem is a good example of this. Sometimes an assumption you're allowed to make (specified in the problem limits) will mean there's a polynomial-time solution (or pseudo-polynomial).

10. Brush up on those math skills

If you feel a bit rusty in math, or you didn't enjoy it in school, it might be time to get out the grid paper and pencil. I did this last year, and wrote a blog post on it. Overall, it was a lot of fun, and it helped to improve my programming and algorithm skills too. I could ramble about this topic for hours, but we don't have time for that, so here are some free resources I used and recommend:

  • Khan Academy: I can't recommend this strongly enough. It covers all of primary (elementary) and high school math in great depth, and it's actually really fun. It's not just for school students, it's equally awesome for adults. Sal has a talent for explaining things and the quizzes and content are broken up into manageable chunks. If you aren't sure where your weaknesses are, have a go at the initial quizzes. They'll tell you where you have work to do. Don't be ashamed if you find there are a few primary school-level concepts that you need to work on. It happened to me, and I was amazed at how much addressing those weaknesses helped me with the more advanced stuff. If you aren't already confident with it, I especially recommend the geometry stuff. The proofs and problem-solving there is a similar kind of thinking to the thinking you're trying to develop on Leetcode.
  • Introduction to Mathematical Thinking: If you feel you possibly have some amount of math-PTSD from school, Keith Devlin is the doctor for you. Keith's mission is to enlighten the general public on what it actually is mathematicians do (i.e. not arithmetic and solving pages and pages of equations for y's x). Aside from that, the course goes over logic and proofs and aims to get you to see the difference between school math and being a mathematician. It's helpful, especially if you aren't keen on tackling the MIT OCW stuff yet.
  • MIT OCW Mathematics for Computer science: This is more advanced, but well worth tackling if it aligns with your goals, i.e. being a top competitive programmer or getting into a top tier tech company. If you did a degree in computer science, you probably already did some discrete math. Unless you did discrete math beyond an introduction at a top school though, then this course will be helpful as there'll almost certainly be stuff you didn't know. At the very least, it's great revision. Note that you'll need to put hours into doing ALL of the “in class” questions to get the maximum benefit. We all learn best from doing, and the questions are of high quality.

11. Be confident knowing the asymptotic complexity of an algorithm, and don't write code until you know your algorithm will be good enough.

When given a question in an interview or a contest, don't dive into writing code. The first thing you'll need to do is think and brainstorm possible ways of solving it. For each one, you need to know how to determine the asymptotic complexity, paying particular attention to the worst cases. You'll then need to look at the problem limits and make a judgement call as to whether or not your algorithm is likely to be fast enough. Over time, you'll build a sense for how long things take. For me, I start to worry when plugging the maximum value into the big-O formula evaluates to over a million or so. It's a very crude measurement, but it seems to serve me well.

Sometimes you'll need to come up with a better algorithm from scratch. Sometimes, you can optimize a bad algorithm so that it's a lot better. But you'll need to be fairly confident before you start coding, otherwise you'll risk wasting time writing and debugging code, only to get the dreaded Time Limit Exceeded error that can only be fixed with a full re-design and re-write.

12. Read the problem limits well.

Often, you'll be told what the maximum input sizes to expect are. These will give you a lot of clues about the kind of algorithm you might be writing. As a very rough rule of thumb:

  • If the limits are tiny, i.e. all under 50 or so, then your algorithm will probably be non-polynomial (and this is a “smell” for intractable problems).
  • If it's under about 1000, then you might be looking at quadratic or around that.
  • If it's over that, but under 1,000,000, then it's probably linear or quasilinear.
  • Over that, and it's quite possibly logarithmic. If it's ridiculously high (e.g. billions or trillions) like sometimes seen in the Google Code Jam, that can be a sign of being able to shrink it with modular arithmetic or even a constant time algorithm.

These aren't certain rules, but they definitely give a good indication. On the other hand, if your quadratic algorithm needs to run on sizes of 2000 or more, it's probably not going to pass. Usually, but not always, quadratic algorithms can be replaced with quasilinear ones taking a divide and conquer approach, or using binary search somehow.

13. Choose the best variable to scale up on.

Sometimes, what seems to be the main variable (e.g. number of items) is quite high, but another variable is surprisingly lower. Going back to one of my favourite questions -- Smallest Sufficient Team -- we are told this:

1 <= req_skills.length <= 16
1 <= people.length <= 60
1 <= people[i].length, req_skills[i].length, people[i][j].length <= 16
Elements of req_skills and people[i] are (respectively) distinct.
req_skills[i][j], people[i][j][k] are lowercase English letters.
It is guaranteed a sufficient team exists.

The interesting detail here is that the maximum number of required skills is a lot lower than the maximum number of people. This is a big clue. It suggests that the cost will primarily scale with the number of skills required, not with the number of people. Instead of an approach that considers including or not including each person, we should instead be looking at a solution where the main looping is over the skill list.

This is common in Google Code Jam questions. It's worth having a really good look and think about the limits, especially if one seems unusually low/ is a surprising constraint. It can often mean there's a good solution that wouldn't have worked as well if it was bigger.

14. Share your solutions and teach others

It's widely known that to master something, you have to teach it. Writing up high-quality solutions and explanations on the discussion forums is a great way of doing this. It will also help you develop other software engineering skills, such as writing and explaining. Make sure you post with the goal of helping others, and not showing off. Nobody likes a code dump with no comments or explanations.

Take pride in the code you share. Very few people like reading code where all the variable names appear to have been taken from the lyrics of the famous alphabet song. When writing your post and the code to put in it, pretend potential employers might be reading it. If you're wanting to get into a top tech company, then write the kind of code you'd be expected to write there. For example, use descriptive variable names. During my Google internship, I noticed this in the Google codebase and my partner James also tells me that Canva's codebase is the same.

ABOVE ALL OTHER ADVICE, put 3 backticks before and 3 backticks after your code. This will make it format nicely. It amazes me how many people don't know to do this/ don't bother to find out how when their code looks hideous and unreadable from the lack of it.

15. Practice general problem solving

When learning algorithms, data structures, and techniques, I recommend you use the tags to find relevant problems. After all, your goal is to master a specific skill.

But after that, you need to practice “problem solving”. This is where you have a problem to solve, and you do not know which of your skills you'll be applying to it. To do this, the weekly contests and mock interview feature are amazing.

Remember that sometimes a question will require a technique you've never seen before, or will require a major adaptation to one. You'll need to learn to be creative and think outside the box if this doesn't come naturally to you, memorizing solutions will not help you. A great example of this is questions such as Minimize Max Distance to Gas Station. There is a quasilinear solution using binary search. It's not at all what you might initially think.

16. Do the weekly contests

These are great fun, and a great chance to practice your general problem solving, although they can be buggy as the questions are new to the site. Cheating is also an issue, particularly in the form of people hardcoding the answers to the largest test cases. This has reduced a lot though since Leetcode stopped displaying the answer to test cases where the result was Time Limit Exceeded. There are also issues with some programming languages being faster than others. Sometimes, a poor solution will slip through with C++ and Java, but not Python, for example.

There is no rule against using previous code, although at the end of the day, one needs to decide what their goal is. Using previous code is not helping you learn. Most of the winners seem to be using previous code, their goal is generally to accumulate Leetcoins so that they can get their yearly subscription and a T-shirt (or so it seems)

It also takes a lot of practice. Over time, you'll get better at it. I wasn't getting in the top 200 at first, it took practice.

17. Use the mock interview feature

I have a Leetcode subscription, so in theory, I could do the company-specific interviews. I actually never do though, I much prefer the random sets. They seem to be taken from the company-specific ones, with the difference being you don't know which company it's from. So don't worry about not having a subscription, you'll still find this feature useful.

As a general rule, the 3 levels (screening, phone interview, and onsite) are more difficult than the last. So, start with the screening ones and then move up from there. Unfortunately, Leetcode will sometimes give you a mock interview you've seen previously, there's not much you can do about this. Once it seems to be happening a lot though, that probably means you've done most of their mock interviews of that type. Because there is a finite number of them, it's also best to not use them until you've done some practice (i.e. mastered at least the basic and intermediate skills I've listed above).

Fun question: If there are 25 mock interview problem sets, and each has the same probability of appearing, around how many mock interviews will you need to do to see them all? How many repetitions can you expect to see?

In all seriousness, the mock interview feature is great for keeping you focused (it's timed!) and for forcing you to solve some problems without seeing their tags. Remember what I said about practicing your general problem solving?

18. Don't spend all your time doing easy-level questions

I used to fall into this trap, I would do easy level questions, trying to do them as fast as I could. Then I realized that was a big waste of time, as it was the medium and hard level questions that I was struggling on in the weekly competitions, not the easy ones. Getting the easy ones solved quickly doesn't mean much compared to getting the points of the hard ones.

You need to do questions that are challenging, even if they take you over an hour. This is how you'll learn and improve. As you get better at them, your speed will go up. Focus first on accuracy and skill, and then secondly on speed. Remember that if you can at least get to the point of answering all 4 competition questions in the 90 minutes available, you're already doing better than most people.

19. Try to avoid writing bugs in the first place

We all know debugging sucks when you're timed and seeing those precious minutes tick by. The best solution is to not write bugs in the first place. By programming mindfully, with your full attention, you'll be able to consciously ensure each line is doing what you want it to.

For particularly tricky lines of code, such as binary search boundaries, make a quick example if you have to, and make sure your code does what it's supposed to on it. Do this as you're writing the algorithm, not at the end. If you don't remember the details of a particular library function perfectly, quickly look it up, don't guess. In an interview, tell your interviewer you can't remember. Most people forget these things, and there's a chance they won't remember either. They can tell you what assumption to make. Also, in competitions, it can help to have a shell up with your chosen language if it's interpreted. Sometimes I forget how particular things in Python work (especially when it involves complex splicing) so being able to quickly type in an example really helps!

Something else that helps me is writing code on paper first. I find there are less bugs, as I can't handwrite as fast as I can type.

20. You must have fun!

You'll burn out in no time if you don't enjoy it. Don't go about your studying in a way that makes it feel like a torturous chore. Do what you need to enjoy it.

And if you still just can't enjoy it, then perhaps it isn't for you, which is fine. We all have things we do and don't enjoy.

Feel free to leave any questions or further tips you might have in the comments!

来源:
https://heidi-newton.com/blog/getting-good-at-leetcode

5 / 5 ( 10 votes )

回归本源一一位运算及其应用(完整全文) – 2014国家集训队论文

2021年9月18日 23:09

回归本源一一位运算及其应用

镇海中学 沈洋

2014年信息学奥林匹克中国国家队候选队员论文

2014国家集训队论文

完整版pdf请从本github仓库 下载

摘要

在电子计算机中,位运算可以说是比四则运算史基本的运算,但由于它们 不是数学的基本运算且不常在生活中应用,容易被部分选手,特别是初学信息 学的选手忽视。本文希望通过对位运算及其应用的介绍,来使这一情况得到一 定的改善。

本文大致可分为三个部分。第一部分(第1节)对位运算基本概念进行 了介绍,第二部分(第2至4节)对位运算的几个应用进行了介绍,第三部分 (第5节)展示了一些例题。

几点说明

  • 约定二进制最高位为最左边,最低位为最右边,最低位记为第0位。

  • 本文中的所有程序均以C++语言描述,所涉及的语言细节、内建函数等均以GNU C++(GCC编译器)为标准。

  • 若无特别说明,本文中的位运算使用C++运算符表示。

  • 本文中汇编指x86汇编。

1 基本运算

1.1 与、或、非和异或

对于逻辑运算的"与"、"或"、"非"和"异或",想必大家都耳熟能详了。

其中"与"(∧)、"或"(∨)、"异或"(⊕)的真值表如下:

p q p ∧ q p ∨ q p ⊕ q
0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 1 0

"非"(¬)的真值表如下:

p ¬p
0 1
1 0

位运算的"与"、"或"、"非"和"异或"可以看作逻辑运算在整数上的扩展。 它们的运算法则如下:

运算结果的每个二进制位的值等于两个运算数的对应二进制位进行逻辑"与"运算的结果。

运算结果的每个二进制位的值等于两个运算数的对应二进制位进行逻辑"或"运算的结果。

运算结果的每个二进制位的值等于运算数的对应二进制位进行逻辑"非"运算的结果。

异或 运算结果的每个二进制位的值等于两个运算数的对应二进制位进行逻辑"异或"运算的结果。

这四种位运算对应的汇编指令如下:

异或
and or not xor

由此可以看出,对于这四种运算,带符号整数和无符号整数并没有对应不同的汇编指令,它们的行为是相同的。 也就是说,对于带符号整数,符号位也会与其它位按照同样的方式处理。

1.2 移位运算

移位运算分为逻辑移位、算数移位、循环移位、带进位循环移位四种。 其中带进位循环移位涉及到CF标志位,因此不在我们的讨论范围内。为了准确区 分各种移位运算,在这一部分中我们使用汇编指令来表示它们。

当我们说将x左移或右移y位时,默认y非负且小于x的位宽1。 例如对于32位整数,y只能在0到31之间取值。

逻辑移位

逻辑左移对应的汇编指令为shl x y,它的功能是把x的每个二进制位向左移动y位,移动造成的最右边的空位由0补足,最左边的数溢出2。

逻辑右移与逻辑左移完全相反,它对应的汇编指令为shr x y,它的功能是把x 的每个二进制位向右移动y位,移动造成的最左边的空位由0 补足,最右边的数溢出。

算术移位

算术左移的汇编指令为sal x y,它与逻辑左移完全相同。

算术右移的汇编指令为sar x y,它与逻辑右移大体相同,唯一的区别在于移动造成的最左边的空位由符号位(最高位)补足而不是由0 补足。

循环移位

循环左移对应的汇编指令为rol x y,它的功能是把x的每个二进制位向左移动y位,移动造成的最右边的空位由最左边溢出的位补足。

循环右移与循环左移完全相反,它对应的汇编指令为ror x y,它的功能是 把x 的每个二进制位向右移动y位,移动造成的最左边的空位由最右边溢出的位 补足。

移位运算的数学意义

在逻辑移位、算术移位这两个看似复杂的定义背后实际上是有其数学含义的。逻辑移位被设计用于处理无符号整数,它的左移和右移分别与无符号整数的x × 2^y 和x ÷ 2^y 具有相同的效果: 算术移位被设计用于处理带符号整数,它的左移和右移分别与带符号整数的x × 2^y 和x ÷ 2^y 具有相同的效果(舍入方式为向 下舍入)。 这一点根据补码的定义比较容易证明,在此就不赘述了。

1若超出这个范围,汇编指令只考虑y的后几位,C语言将产生未定义行为。

2实际上,最后一个溢出的位会被放入标志位CF中,其他标志位也会相应发生变化,但这不在我们的讨论范围内,故在本文中略去对这些部分的描述。

C++中的移位运算

在C++中,无符号整数的移位使用逻辑移位,有符号整数的移位使用算术 移位。这样一来,无论是无符号整数还是带符号整数,我们都可以放心的使用 左移和右移来代替乘以二的幕或除以二的幕的操作。

C++中没有专门的对带符号整数进行逻辑右移的运算符,不过我们可以通过强制类型转换将它转换成无符号整数后再进行运算。

C++中没有提供循环移位操作符,但我们可以通过其他运算的组合来实现它。 例如对于32位无符号整数x,表达式(x << y) | (x >> (32 - y))可以实现循环左移的功能,而(x >> y) | (x << (32 - y))则可以实现循环 右移的功能。

2 二进制位的修改和查询

借由汇编指令、 内建函数以及基本位运算的组合,我们可以在二进制位的 层面上对整数进行一些修改和查询。 本节将介绍一些常用操作并给出一些易于 理解和扩展的实现3。 由于带符号整数的性质较为复杂,在这里我们只讨论无符号整数的相关操作,并以常用的32位无符号整数为例。

2.1 读某些位

读取x的第pos个二进制位是一个很常用的操作,它的实现方式是先将x右移pos位,使要读取的位移到最低位,再通过 & 1 将其取出。 代码如下4:

bool readBit(u32 x, int pos) {
    return (x >> pos) & 1;
}

3一些"黑技术"如利用64位指令处理32位操作数、 利用浮点数进行计算等并未涉及。 对此感兴趣的读者可以参考 http://graphics.stanford.edu/~seander/bithacks.html

4为简化代码,我们使用u32来表示32位无符号整数(unsigned int)。

有时候我们会将多个整数"打包"在一个整数中,一个典型的应用是将四 个字节打包为一个32位整型,那么读取的时候就需要形如"读x的第pos 位开始 的cnt位"这样的操作。 它的实现方式与上面别无二致: 首先将x右移pos位,再 通过& mask 来取出最后cnt位,其中mask的后cnt个位为1,其余位为0,可以 由(1 << cnt) - 1得到。 代码如下:

u32 readBits(u32 x, int pos, int cnt) {
    return (x >> pos) & ((1u << cnt) - 1);
}

通过上面两个例子,不难发现读取某个或某些二进制位的关键实际上是与 运算。 通过将原数和一个遮罩mask进行与运算,可以达到保留指定一些位(将遮罩mask的对应位设为1),清零其它位(遮罩mask的对应位设为0)的目的。

2.2 改某些位

对二进制位的修改实际上是"与"、"或"和"异或"运算的实际应用。

将某些位置为1

要实现这个功能,我们的办法是将原数与一个遮罩进行或运算。 对于要改为1 的位,我们将遮罩的对应位设为1,否则将对应位设为0,这样,原数与遮罩 进行或运算的结果就是答案。举例来说,将x的第pos 位置为1的代码如下:

u32 setBit(u32 x, int pos) {
    return x | (1u << pos);
}

将某些位置为0

这个操作与上一个操作正好相反,这一次我们要利用的是与运算。 我们构 造一个遮罩,对于要修改的位,我们将遮罩的对应位设为0,否则将其设为1 (注意,这与上一个操作中的遮罩完全相反),随后将原数与这个遮罩进行与运 算即可得到答案。 将x的第pos位置为0 的代码如下:

u32 clearBit(u32 x, int pos) {
    return x & ˜(1u << pos);
}

取反某些位

这一次是异或运算的应用。 同样构造一个遮罩,如果我们要取反某位,则 将遮罩的对应位设为1,否则将其设为0,随后将原数与这个遮罩进行异或运算 即可得到答案。 将x的第pos位取反的代码如下:

u32 flipBit(u32 x, int pos) {
    return x ^ (1u << pos);
}

2.3 求1的个数

分治法

求二进制位中1的个数与求各个二进制位的和是等价的,因此我们转而思考 如何求各个二进制位的和。 我们可以使用分治来解决这个问题:每次将整个数 分成两个部分,分别求出每个部分的和,再将它们相加。 利用位运算,我们可 以并行地完成每一层的工作,像下面这样:

int bitCount_1(u32 x) {
    x = ((x & 0xAAAAAAAAu) >> 1) + (x & 0x55555555u);
    x = ((x & 0xCCCCCCCCu) >> 2) + (x & 0x33333333u);
    x = ((x & 0xF0F0F0F0u) >> 4) + (x & 0x0F0F0F0Fu);
    x = ((x & 0xFF00FF00u) >> 8) + (x & 0x00FF00FFu);
    x = ((x & 0xFFFF0000u) >> 16) + (x & 0x0000FFFFu);
    return x;
}

它是这样工作的:

  • 第一步,有32个项需要相加,每一项占1 bit。 我们将其中的奇数项和偶数 项分别取出来(利用2.1 小节中提到的方法),并将奇数项右移1位和偶数 项"对齐",然后将他们相加。 这一步过后我们实际上将16对1 bit的项分 别相加,并将结果存放在了原来这两项所在的2 bit空间上。

  • 第二步,有16个项需要相加,每一项占2 bit。 我们将奇偶项分别相加,形成8个4 bit的项。

​ ...

  • 第五步,有两项需要相加,每一项占16 bit。 将这两项相加我们便得到了答案。

那么,还能再优化么?答案是肯定的。 看下面这段代码:

int bitCount_2(u32 x) {
    x-= ((x & 0xAAAAAAAAu) >> 1);
    x = ((x & 0xCCCCCCCCu) >> 2) + (x & 0x33333333u);
    x = ((x >> 4) + x) & 0x0F0F0F0Fu;
    x = ((x >> 8) + x) & 0x00FF00FFu;
    x = ((x >> 16) + x) & 0x0000FFFFu;
    return x;
}

这段代码较上一段的第一个差别是第一步的实现。 我们知道第一步的作用

是将奇数位和偶数位相加,根据第一个程序它的结果应为:

((x & 0xAAAAAAAAu) >> 1) + (x & 0x55555555u)

而x的值等于:

((x & 0xAAAAAAAAu) + (x & 0x55555555u)

将两式作差得到:

(x & 0xAAAAAAAAu) >> 1

因而将x减去(x & 0xAAAAAAAAu) >> 1便可以得到我们所求的结果。

另一个差别是第二步之后的实现,我们以第二步为例来说明。 第二步的作 用是将4对4 bit整数相加,但实际上此时每个整数最大只可能是4,这就表示, 即使是两个数相加的结果也能在4 bit的空间存下。 因此(x >> 4) + x可以正确地依次求出第0个数加第1个数,第1个数加第2个数,第2个数加第3个数……

我们从中取出我们需要的结果即可。

还能继续优化么?答案依然是肯定的:

int bitCount_3(u32 x) {
    x-= ((x & 0xAAAAAAAAu) >> 1);
    x = ((x & 0xCCCCCCCCu) >> 2) + (x & 0x33333333u);
    x = ((x >> 4) + x) & 0x0F0F0F0Fu;
    x = (x * 0x01010101u) >> 24;
    return x;
}

这段代码与上一段的不同点在于,上一段代码的最后两步操作被语句:

x = (x * 0x01010101u) >> 24

代替了。 我们知道上一段代码中第四、 第五步的作用是将x中的4个8 bit数相加,

那么,如果我们令:

x = (a<<24)+(b<<16)+(c<<8)+(d<<0)

则我们所求的答案便是:

a + b + c + d

我们来看看计算x * 0x01010101u 时发生了什么:

x*0x01010101
=(x<<24)+(x<<16)+(x<<8)+(x<<0)
=(a<<48)+(b<<40)+(c<<32)+(d<<24)+
         (a<<40)+(b<<32)+(c<<24)+(d<<16)+
                 (a<<32)+(b<<24)+(c<<16)+(d<< 8)+
                         (a<<24)+(b<<16)+(c<< 8)+(d<< 0)
=((a)   <<   48) +
((a+b)  <<   40) +
((a+b+c) <<  32) +
((a+b+c+d)<< 24) +
((b+c+d) <<  16) +
((c+d)   <<  8 ) +
((d)      << 0 )

由于x是32位整数,因此上式的前二项应当溢出,于是x 0x01010101的结果为((a+b+c+d)<<24) + ((b+c+d)<<16) + ((c+d)<<8) + (d)考虑 到a,b,c的实际意义,它们的值都不会超过8,因此上式的后二项的值必定 小 于1 << 24,也就是说它们不会对结果的最高8位造成任何影响。因此,x 0x01010101的结果的高8位便是我们所求的a+b+c+d,我们将它右移24位即可得到答案。

查表法

如果我们要求0到n中每一个数的答案的话,那么可以使用如下递推:

f(0) = 0,
f(i) = f(i >> 1) + (i & 1)

显然对于所有32位整数保存答案(至少在目前看来)是一件不太现实的事 情,不过好在这个问题是可以分割的,我们可以预处理所有16位整数的答案, 在询问时将被询问的整数拆成高16位和低16位分别计算答案。 于是预处理的代 码如下:

int cnt_tbl[65537];
void bitCountPre() {
    cnt_tbl[0] = 0;
    for (int i=1; i<65536; ++i)
        cnt_tbl[i] = cnt_tbl[i >> 1] + (i & 1);
}

回答询问的代码如下:

int bitCount_4(u32 x) {
    return cnt_tbl[x >> 16] + cnt_tbl[x & 65535u];
}

内建函数

GCC中提供了如下内建函数5来实现这个功能:

int __builtin_popcount (unsigned int x);

对于支持SSE4.2的机器,如果在编译时开启相应开关,则该函数会被翻译成汇编指令popcnt,否则该函数使用类似上面"查表法"的方法进行计算。

2.4 翻转位序

对于一个32位整数来说,翻转位序是指将它的第0位与第31位交换,第1位 与第30位交换,…… ,第i 位与第31 − i位交换,…… ,第15 位与第16 位交换。 例如对于32 位整数5,对它进行翻转位序将得到2684354560。

分治法

我们可以采用分治法来解决这个问题:首先将整个数分割成两个部分,分别翻转这两个部分,再将这两个部分对调。 同样地,借由位运算,我们可以把 每一层的工作并行完成,实现如下:

u32 bitRev_1(u32 x) {
    x = ((x & 0xAAAAAAAAu) >> 1) | ((x & 0x55555555u) << 1);
    x = ((x & 0xCCCCCCCCu) >> 2) | ((x & 0x33333333u) << 2);
    x = ((x & 0xF0F0F0F0u) >> 4) | ((x & 0x0F0F0F0Fu) << 4);
    x = ((x & 0xFF00FF00u) >> 8) | ((x & 0x00FF00FFu) << 8);
    x = ((x & 0xFFFF0000u) >> 16) | ((x & 0x0000FFFFu) << 16);
    return x;
}

这段代码是这样工作的:

  • 第一步,交换相邻两位。 这样就形成了16个长度为2的己经翻转的组。

  • 第二步,每两位为一组,交换相邻两组。 这就将16个长度为2的组变成了8个长度为4的己翻转的组。

……

  • 第五步,每16位为一组,交换相邻两组。 整个数翻转完成。

5关 于 本 文 中 提 到 的GCC内 建 函 数 的 具 体 说 明 及 其 他 相 关 内 建 函 数, 请 参 阅GCC于 册:

http://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html

查表法

我们首先预处理出所有16位整数翻转后的结果,这可以由如下递推得到:

f(0) = 0,
f(i) = (f(i >> 1) >> 1) | ((i & 1) << 15)

接下来,我们将待翻转的32位整数拆成两个16位整数,通过查表来得到这 两个16位整数的答案,再将它们对调便可以完成整个32位整数的翻转。 预处理 的代码如下:

u32 rev_tbl[65537];
void bitRevPre() {
    rev_tbl[0] = 0;
    for (int i=1; i<65536; ++i)
        rev_tbl[i] = (rev_tbl[i>>1]>>1) | ((i&1)<<15);
}

查询的代码如下:

u32 bitRev_2(u32 x) {
    return (rev_tbl[x & 65535] << 16) | rev_tbl[x >> 16];
}

2.5 求前缀/后缀0的个数

二分法

求前缀0与求后缀0是类似的,我们不妨以求前缀0为例。 首先给出代码:

int countLeadingZeros_1(u32 x) {
    int ans = 0;
    if (x >> 16) x >>= 16; else ans |= 16;
    if (x >> 8) x >>= 8; else ans |= 8;
    if (x >> 4) x >>= 4; else ans |= 4;
    if (x >> 2) x >>= 2; else ans |= 2;
    if (x >> 1) x >>= 1; else ans |= 1;
    return ans + !x;;
}

这段代码的实质是二分查找。 它是这样工作的:

  • 第一步,判断高16位是否为空。 若是,则高16位必然均为前缀0,我们给 答案加上16,并在第0至15位上继续二分:若不是,则前缀0只出现在则 高16位上,我们将它们右移到低16位上以便对它继续二分。

  • 第二步,判断此时的高8位是否均为前缀0,并选择一边继续二分下去。

    ……

  • 第六步,判断仅剩的那一位是否为0,若是,则给答案加1。

求后缀0个数的方式与此别无二致,因此根据上一段代码炮制一段求后缀0个数的代码并不是件难事:

int countTrailingZeros_1(u32 x) {
    int ans = 0;
    if (!(x & 65535u)) x >>= 16, ans |= 16;
    if (!(x & 255u )) x >>= 8, ans |= 8;
    if (!(x & 15u )) x >>= 4, ans |= 4;
    if (!(x & 3u )) x >>= 2, ans |= 2;
    if (!(x & 1u )) x >>= 1, ans |= 1;
    return ans + !x;
}

这段代码的工作原理与上一段基本相同,就不再赘述了。

查表法

我们依然以前缀0个数为例。 这个问题与同样是可以分割的。 我们可以先确 定第一个1出现在高16位中还是低16位中,再通过查表来得到这个16 位整数的 答案。 这张表可以通过如下递推得到:

f(0) = 16,
f(i) = f(i >> 1) - 1

于是我们就不难写出预处理的代码:

int clz_tbl[65537];
void countLeadingZerosPre() {
    clz_tbl[0] = 16;
    for (int i=1; i<65536; ++i)
        clz_tbl[i] = clz_tbl[i >> 1] - 1;
}

以及查询的代码:

int countLeadingZeros_2(u32 x) {
    if (x >> 16)
        return clz_tbl[x >> 16]; else
        return clz_tbl[x & 65535u] + 16;
}

求后缀0个数也可以通过几乎相同的方法实现。

内建函数

对于这两个问题,GCC也提供相应的内建函数。

求前缀0的内建函数为:

int __builtin_clz (unsigned int x);

它对应的汇编指令为bsr (Bit Scan Reverse)外加一个异或。

求后缀0的内建函数为:

int builtin_ctz (unsigned int x);

它对应的汇编指令为bsf (Bit Scan Forward)。

需要注意的是,当参数x为0时,这两个函数的行为是未定义的。

2.6 求第k个1的位置

这个问题可以转化为求最大的w,使得第0位到第w − 1中1的个数小于k。 这 显然是可以二分的。 于是,借助2.3小节中的cnt_tbl,我们便能实现一个简单的二分:

int kthBit_1(u32 x, int k) {
int ans = 0;
    if (cnt_tbl[x & 65535u] < k)
        k -= cnt_tbl[x & 65535u], ans |=16, x >>=16;
    if (cnt_tbl[x & 255u ] < k)
        k -= cnt_tbl[x & 255u ], ans |= 8, x >>= 8;
    if (cnt_tbl[x & 15u ] < k)
        k -= cnt_tbl[x & 15u ], ans |= 4, x >>= 4;
    if (cnt_tbl[x & 3u ] < k)
        k -= cnt_tbl[x & 3u ], ans |= 2, x >>= 2;
    if (cnt_tbl[x & 1u ] < k)
        k -= cnt_tbl[x & 1u ], ans |= 1, x >>= 1;
    return ans;
}

不借助cnt_tbl自然也是可以的。 我们可以发现,我们在二分的过程中所需要的区间和都是2.3小节中分治法计算过的区间,因此我们可以将分治的中间 值存下来供二分时使用,像下面这样:

int kthBit_2(u32 x, int k) {
    int s[5], ans = 0, t;
    s[0] = x;
    s[1] = x - ((x & 0xAAAAAAAAu) >> 1);
    s[2] = ((s[1] & 0xCCCCCCCCu) >> 2) + (s[1] & 0x33333333u);
    s[3] = ((s[2] >> 4) + s[2]) & 0x0F0F0F0Fu;
    s[4] = ((s[3] >> 8) + s[3]) & 0x00FF00FFu;
    t = s[4] & 65535u;
    if (t < k) k -= t, ans |=16, x >>=16;
    t = (s[3] >> ans) & 255u;
    if (t < k) k -= t, ans |= 8, x >>= 8;
    t = (s[2] >> ans) & 15u;
    if (t < k) k -= t, ans |= 4, x >>= 4;
    t = (s[1] >> ans) & 3u;
    if (t < k) k -= t, ans |= 2, x >>= 2;
    t = (s[0] >> ans) & 1u;
    if (t < k) k -= t, ans |= 1, x >>= 1;
    return ans;
}

2.7 提取末尾连续的1

我们知道对x加1之后,最右边连续的1会变为0,最右边的0则变为1,而其

它位不变。 利用这一点我们可以通过下面这个公式来提取x末尾连续的1:

x & (x ^ (x + 1))

2.8 提取lowbit

正整数x的lowbit是指x在二进制下最右边一个1开始至最低位的那部分,记 为lowbit(x)。 例如28 (11100B)6的lowbit 为4 (100B),16 (10000B) 的lowbit则为16 (10000B)。 lowbit的典型应用是树状数组和遍历集合。

有如下3个公式能帮我们求解lowbit:

lowbit(x) = x & (x ^ (x - 1))
lowbit(x) = x ^ (x & (x - 1))
lowbit(x) = x & -x

前两个公式利用了"将x减去1后,x中最右侧的1变为0,其之后的0变为1" 这一点,第3个公式则主要利用了补码的特性。读者可以自行完成这3个公式的证明。

2.9 遍历所有1

一般情况下,我们可以通过不断求lowbit并将它从原数中删去(通过异或) 来遍历每一个1。 当需要用到这个1所在的位置时,我们可以通过2.5小节中介绍 的求后缀0个数的方法来获知。

3 将整数用作集合

集合的种类有很多,但将它们的元素标号之后,都能映射到整数集合上。

因此在这里我们只讨论全集为[0, n)的整数集合。

二进制的每个位有0和1两种状态,这正好可以对应集合中某个元素是否存 在,因此我们可以用二进制数来表示集合一一如果某位为1就表示对应元素存在 于这个集合中,否则不存在。然而计算机中单个二进制数的位数是有限的,不 妨设其能处理的单个二进制数的最大位宽为w,那么,表示上述全集为[0, n)的 整数集合就需要至少in/wl个二进制数。 我们把每个二进制数叫做一"块",并 规定第i块记录第wiw(i + 1) − 1这个w 个数是否存在(0 ≤ i < in/wl,最后一块 略有不同)。 这样,我们便得到了一个维护集合的数据结构一一bitset。 我们将某 个bitset能表示的全集的大小称为该bitset的大小。

6我们用后缀B来表示一个二进制数。

从本质上来说,bitset实际上是压w位的二进制高精度整数,因此也可以进行与、或、非、异或、左移、右移这些位运算,甚至可以进行加减乘除。

3.1 加入某个元素

首先计算出被加入元素所属的块,再使用2.2小节所述的方法将对应位置为1。

时间复杂度: O(1)

3.2 删除某个元素

首先计算出被删除元素所属的块,再使用2.2小节所述的方法将对应位置为0。

时间复杂度: O(1)

3.3 查询某个元素是否存在

首先计算出待查询元素所属的块,再利用2.1小节所述的方法检查对应位是否为1。

时间复杂度: O(1)

3.4 交集

集合的交和bitset的与运算相对应。 x与y的交为x & y。

时间复杂度:O(n/w)

3.5 并集

集合的交和bitset的或运算相对应。 x与y的并为x | y。

时间复杂度:O(n/w)

3.6 差集

记A、B是两个集合,则所有属于A且不属于B的元素构成的集合叫做A与B的差。集合的差可以通过bitset的与运算和异或运算实现。x与y的差为x ^ (x & y)。

时间复杂度:O(n/w)

3.7 补集

集合的补与bitset的非运算相对应。 x的补为∼x。

时间复杂度: O(n/w)

3.8 统计元素个数

对于每个块分别使用2.3小节中介绍的方法计算1的个数,再将所有块的答案相加。

时间复杂度:O(n/w)

3.9 遍历集合元素

遍历所有块,对每个块使用2.9小节中介绍的方法遍历元素。

时间复杂度:O(n/w + cnt),其中cnt表示集合的元素个数。

3.10 求集合中第k小元素

朴素做法

首先从小到大依次遍历每个块,找出第k小元素所在的块,再利用2.6 小节中介绍的方法确定具体是哪个元素。

时间复杂度:O(n/w + log w)

更加高效的做法

在某些应用中,求第k小元素可能比其它集合操作的使用频率更大,此时我 们就会需要一个复杂度更优的查询第k小元素操作,同时要尽可能小地影响其它 集合操作的复杂度。

一种改进方式是利用线段树。 我们使用一颗辅助线段树来维护bitset中每个 块的元素个数。 这样一来,我们就可以通过在线段树上二分的方式快速确定 第k小元素所属的块。 这是一个经典的线段树操作,在这里就不再赘述。 找出第k小元素所在的块之后,我们就可以利用2.6小节中介绍的方法确定具体是哪个元素了。 这样改进之后,求第k小元素的时间复杂度降为了O(log n)。

添加了辅助线段树之后,其他的集合操作也要做出相应修改。 在加入、 删 除集合元素时,该元素所属的块的元素个数会发生改变,我们需要同时维护辅 助线段树的信息,因此这两个操作的时间复杂度变为了O(log n)。 在集合求交、 并、 差、 补的时候,我们可以直接暴力重建辅助线段树,时间复杂度维持不变 为O(n/w)。

3.11 求集合中大于x的最小元素

朴素做法

首先判断x所在块中是否存在大于x的元素,若是则直接返回这个元素,否 则从x所在的块开始向后遍历,找到x之后第一个包含元素的块,然后返回该块 的第一个元素。 其中,某块的第一个元素以及某块中比x大的第一个元素均可以 利用2.5小节中介绍的统计后缀0个数的方法求出。

时间复杂度:O(n/w)

更加高效的做法

类似于求第k小元素,我们也可以利用辅助数据结构来加速大于x的最小元素的查询。

这次我们利用的是bitset本身。 我们使用一个大小为n/w的辅助bitset7来保存每个块是否有元素,这样一来,我们可以通过如下流程来完成该查询:

  1. x所在块中存在大于x的元素,则直接返回这个元素,否则转2。

  2. 利用辅助线段树查询x所在块之后第一个含有元素的块,转3。

  3. 在该块中找第一个元素并返回结果。

改进之后,该查询的时间复杂度降为了O(logw n)。

同样地,其他的集合操作也要做出相应修改。 在加入、 删除集合元素时,我们需要同时维护辅助bitset中该元素所在块的信息,因此这两个操作的时间复杂度变为了O(logw n)。 在集合求交、 并、 差、 补的时候,我们可以直接暴力重建辅助bitset,时间复杂度维持不变为O(n/w)。

7这里的bitset是递归定义的,因此这个辅助bitset也支持快速求集合中大于x的最小元素。

3.12 将集合的全部或部分元素加上/减去x

将集合的全部元素加上/减去x

对集合a的每个元素都加上x只需要将a左移x位即可。 同样的,对集合a的每个元素都减去x 只需要将a右移x位即可。

时间复杂度:O(n/w)

将集合的部分元素加上/减去x

设原集合为a,要求改动的那部分元素组成的集合为b。 在这次修改中, a与b的差这部分元素是不会改变的,记为集合p:a与b的交这部分元素是需要 加上/减去x的,记为集合q。 之后我们利用移位操作对集合q作相应修改,再与 集合p求并即可。

时间复杂度:O(n/w)

3.13 枚举子集

枚举子集的关键在于,对于某个集合的某个子集,我们需要求出字典序排 在它前一位的集合或后一位的集合。 对于集合x和它的一个子集y,我们可以通 过(y - 1) & x来求出字典序排在它前一位的子集。因此,若要枚举x的子集, 我们只需从x本身开始,不断求前一个子集,直到枚举到空集为止即可。

3.14 枚举含有k个元素的集合

与枚举子集类似,枚举含有k个元素的集合的关键同样是对于某个含有k个 元素的集合求出字典序排在它前一位或后一位的集合。 这一次我们选择的是构 造字典序后一位的集合。

朴素的做法如下:设当前集合中的数从小到大依次为a0, a2, . . . , ak−1,其中 最小的可以增大的数为a j,那么我们把a j 加上1,并将a0到a j−1重置为0到 j − 1即 可得到下一个包含k 个元素的集合。

这个做法反映到bitset上是这样的: 设最右边连续的1是第i位到第 j位,那么 我们要做的就是将第 j位上的1向左移一位,同时将第i位到第 j − 1位上的1向右移 到最右边。 这可以由下面这段代码完成:

u32 nextComb(u32 x) {
    u32 l = x & -x, y = x + l;
    return y | (((x ^ y) / l) >> 2);
}

它是这样工作的:

  • 第一步,使用l = x & -x求出x的lowbit。

  • 第二步,y = x + l将第 j位上的1向左移动一位,同时将第i位到第 j−1位置为0(ij的含义见上文)。

  • 第三步,((x ^ y) / l) >> 2将第i位到第 j − 1位上的1提取出来并移到最右边。

  • 第四步,将这些1与y合并得到答案。

4 其他应用

除上两节所介绍的应用外,我们还可以利用位运算做一些其他事情。

4.1 判断奇偶性

根据奇偶性的定义,二进制最低位为0的数为偶数,最低位为1的数为奇数, 因此我们可以直接使用x & 1 来判断x的奇偶性。 若x & 1等于1则表明x为奇 数,否则x 为偶数。

4.2 乘以或除以二的幕

根据移位运算的实际意义,我们可以直接利用左移和右移实现,参见1.2 小节中的相关介绍。

4.3 对二的幕取模

x mod 2^y 相当于取出x的后y位,因此可以用x & ((1 << y) - 1)来实现相同的效果。

4.4 求log2 x的整数部分

x的位宽为w, 使 用2.5小节中介绍的方法计算出的前缀0个数为l, 那么 ⌊log2 x⌋ = w − 1 − l

4.5 交换两个数

这是异或的经典应用。 我们可以通过如下代码来交换a, b:

a ^= b, b ^= a, a ^= b

4.6 比较两个数是否相等

我们可以计算两个数的异或值,若等于零则说明这两个数相等,否则说明这两个数不相等。

4.7 取绝对值

对于一个w位带符号整数x,它的符号位sign = x >> (w - 1),我们可 以通过表达式(x ^ -sign) + sign来得到x的绝对值。 这一点根据补码的定 义比较容易证明,在此就不赘述了。

4.8 选择

我们可以利用表达式y ^ ((x ^ y) & -cond)来实现选择的功能:

  • cond = 1,其结果为x

  • cond = 0,其结果为y

5 例题

5.1 筷子

试题来源

经典问题

试题大意

有2n + 1个整数,其中某个数出现了奇数次,其他数出现了偶数次。 求出现了奇数次的那个数。

算法介绍

由于异或运算满足交换律,并且满足x ⊕ x = 0,因此将所有数异或起来即为答案。

5.2 Robot in Basement

试题来源

Codeforces 97D8

Yandex.Algorithm 2011 Finals

试题大意

在一个n × m (n, m ≤ 150)的网格上,有一些格子是障碍,并且网格边界上的格子均是障碍,另有一个非障碍的格子是出口。 有一个机器人可以根据程序在 该网格上行走。 一段程序是一个由UDLR 四种指令组成的字符串,机器人会依 次执行每个指令,一个指令会使机器人向指定的方向移动一格,如果对应格子 为障碍则不动。

现在给定一个长度为 l (l ≤ 10^5)的程序,求它的一个最短前缀p,使得对于一开始在网格图上任意非障碍位置的机器人,在执行完程序p之后都停在出口上。

8 http://codeforces.com/problemset/problem/97/D

算法介绍

首先不难想到一个O(nml)的朴素算法。 我们直接顺序执行给定的程序,并 按照题目指定的移动方式暴力维护网格上哪些格子有机器人,直到执行完某一 步后发现网格上只有出口位置有机器人时结束。

实际上我们可以利用bitset来维护网格上哪些格子有机器人。 一种表示方式 是利用bitset的第i · m + j个位置表示格子(i, j) (0 ≤ i < n, 0 ≤ j < m)上是否有机器 人。这样一来一个向左或向右移动的指令就对应了该bitset的左移或右移1位,一 个向上或向下的指令就对应了该bitset的左移或右移m位。 障碍的问题可以这样 解决:预处理cL, cR, cU, cD四个bitset,分别表示向左、 向右、 向上及向下走一 步会碰到障碍的位置。 以向左走为例,设原bitset为x,那么执行一个指令L之后 会撞到障碍的位置为x & cL,能正常向左走的位置为x ^ (x & cL),我们 知道能正常向左走的那些机器人应该被左移一位,而那些会撞到障碍的机器人 则应留在原地,因此最终的bitset应变为(x & cL) | (x ^ (x & cL))。 改进后的算法的时间复杂度为 O( nml / w),可以通过本题。

5.3 Quick Tortoise

试题来源

Codeforces 232E9

试题大意

在一个n × m (n, m ≤ 500)的网格上,有一些格子是障碍。共有q (q ≤ 6·10^5)个询问,每次询问是否能只通过向下走和向右走从格子(x1, y1)走到格子(x2, y2)。

算法介绍

首先考虑所有询问满足x1≤px2的情况。对于所有在第p行上方的格子(x,y),求它能走到第p行的哪些格子,己这个点集为*fx,y,它可以通过递推式fx,y=fx+1,yfx,y+1求出。类似地,对于所有在第p行下方的格子(x,y),求第p行的哪些格子能走到它,己这个点集为gx,y,它可以通过gx,y=gx−1,ygx,y−1求出。求出fx,ygx,y之后,我们就可以来处理询问了。对于询问(x1,y1),(x2,y*2),

若fx1,y1∩gx2,y2≠∅,则表示可以从(x1,y1)走到(x2,y2),反之则不能。

9 http://codeforces.com/problemset/problem/232/E

对于一般情况,我们可以使用分治转化成上面的情况进行求解。 例如我们正在处理所有满足l ≤ x1 ≤ x2 ≤ r的询问,令mid = ⌊(l+r)/2⌋,我们可以先按照上一段所述的方法处理所有满足l ≤ x{1} ≤ mid ≤ x{2} ≤ r的询问,再递归地处理剩下的满足l ≤ x1 ≤ x2 < mid的询问和满足mid < x1 ≤ x2 ≤ r的询问。 如果我们只对x轴进 行分治,那么这个算法时间复杂度为O( n·m^2 log n + q log n)。 如果我们交替对x轴 和y轴进行分治,它的时间复杂度可以进一步降为O((nm*)^1.5 + q log nm)。

5.4 Bags and Coins

试题来源

Codeforces 356D10

试题大意

n (n ≤ 70000)个包,每个包可以直接放在地上,也可以放在其他包里面, 并且允许多层嵌套。 有s (s ≤ 70000)个硬币分布在这n个包里。 如果拿出某个硬 币必须要打开第i个包,我们就称该硬币在第i个包里。 现在己知每个包里的硬币 数,第i个包里有*ai (ai* ≤ 70000)个硬币。 求一种满足条件的包的嵌套方式以及硬 币分布方式,或确定问题无解。

算法介绍

不妨设a1 ≥ a2 ≥ . . . ≥ *a*n

我们首先考虑s = a1的情况。 在这种情况下,我们只需要将第1个包放在地上,并在第i (1 ≤ i < n)个包中放入ai − ai+1 个硬币以及第i + 1个包,最后在第n个包中放入*a*n个硬币即可。

于是对于一般情况,即sa1的情况,问题就转化成了边一些包放在地 上(第1个包是必边的),让它们的硬币个数之和等于s。 这是一个经典的背包问 题。 我们可以用一个bitset来表示当前背包(通过当前添加的物品能够达到哪些总体积),设当前bitset为S,那么添加一个体积为x的物品后的bitset就会变 为S | (S << x)。 当添加完所有物品之后,我们检查最终的bitset中是否包 含s这个元素就能确定是否有解。

10 http://codeforces.com/problemset/problem/356/D

如何记录方案呢?我们引入一个 from数组,其中 from[i]表示在某种体积 达 到i的 方 案 中 最 后 加 入 的 物 品。 这 样 一 来,如 果 问 题 有 解,我 们 就 可 以 通 过 from数组不断追溯到上一个加进来的物品,也就可以恢复方案了。 from 数组 的维护也很容易,在添加第i个物品的时候,设添加之前的bitset为S,之后为Sn, 己Sn与S的差为D,我们枚举D的每个元素 j,并将 from[ j] 设为i即可。 由于D的 实际意义是加入第i个物品时新产生的可行体积的集合,因此 from数组的每个元素至多被设置一次,也就保证了复杂度。

该算法的时间复杂度为 O(n*s/w)。

参考文献

[1] Sean Eron Anderson, “Bit Twiddling Hacks”.

[2] Christer Ericson, “Advanced bit manipulation-fu”.

[3] 刘汝佳, 黄亮, <算法艺术与信息学竞赛), 清华大学出版社。

4.3 / 5 ( 12 votes )

从零开始刷力扣 – 刷题列表【玩转力扣】

2021年9月7日 19:40

从零开始刷力扣 - 刷题列表

noone
发布于 2020-07-09

简介

是不是有许多小伙伴在刷力扣的时候感觉无从下手?从头按顺序开始刷的童鞋们可能会比较有感触,为什么才第四题就感觉很难了?没关系,本文将对力扣的 1-700 题中不需要会员的数据结构与算法题目(数据库与 shell 除外)进行分类,并推荐一个刷题的顺序。

完全零基础可以刷题吗?

不能,至少要基本掌握一门计算机语言的语法。但现在在网上随便搜一下就能搜到许多关于计算机语言的教程。当然,最好还是上一下正规的课程。

刷题顺序很重要吗?

重要。按照题目类别结构化地刷题的速度不仅更快,而且可以在刷完一类题之后进行总结。对于水平较高的小伙伴们来说,按照推荐的顺序刷,可以在 200 小时内刷完 500 多题。对于萌新们来说,按照推荐顺序刷,能更好地掌握数据结构与算法基础。

题目分类及刷题顺序推荐

一. 数组

题目分类 题目编号
数组的遍历 485495414628
统计数组中的元素 64569744844241274
数组的改变、移动 453665283
二维数组及滚动数组 118119661598419
数组的旋转 189396
特定顺序遍历二维数组 5459498
二维数组变换 5664873289
前缀和数组 303304238
题解 数组篇

二. 字符串

题目分类 题目编号
字符 520
回文串的定义 125
公共前缀 14
单词 43458
字符串的反转 344541557151
字符的统计 38738938324249451423657551696467535
数字与字符串间转换 2994125065395535375926403844381312273165481
子序列 392524521522
高精度运算 666741543306
字符串变换 482668
字符串匹配 28686459214
中心拓展法 5647

三. 数与位

题目分类 题目编号
数字的位操作 79479564231342326504263190191476461477693393172458258319405171168670233357400
简单数学题 49229507
快速幂 50372

四. 栈与递归

题目分类 题目编号
用栈访问最后若干元素 68271388
栈与计算器 150227224
栈与括号匹配 2063659132
递归 385341394

五. 链表

题目分类 题目编号
链表的删除 20323719
链表的遍历 430
链表的旋转与反转 61242069225
链表高精度加法 2445
链表的合并 2123

六. 哈希表

题目分类 题目编号
哈希表的查找、插入及删除 217633349128202500290532205166466138
哈希表与索引 1167599219220
哈希表与统计 59435055460945418
哈希表与前缀和 560523525

七. 贪心算法

题目分类 题目编号
数组与贪心算法 60512112256145557513540962117956572284524356464064816921575324517649678420
子数组与贪心算法 53134581152
子序列与贪心算法 334376659
数字与贪心 343
单调栈法 4965034563164023218485

八. 双指针法

题目分类 题目编号
头尾指针 3456801671516181142
同向双指针、滑动窗口 272680838261118764367420934385674247630
分段双指针 8632816088475
快慢指针 141142143234457287

九. 树

题目分类 题目编号
树与递归 10022210122643756361750857254365468787
树的层次遍历 102429690559662671513515637103107257623653104111112113129404199655116117
树的前序遍历 144589
树的前序序列化 606331652297449
树的后序遍历 145590
树的中序遍历与二叉搜索树 947005305382309817366945011095108109
重构二叉树 105106
二叉树的展开 114
最近公共祖先 235236
Morris中序遍历 50199
四叉树 558427

十. 图与搜索

题目分类 题目编号
图的建立与应用 565
深度优先搜索 17397
回溯法 526401363751527739216404647315566049178907993332
回溯法与表达式 241282679
回溯法与括号 22301
回溯法与贪心 488
广度优先搜索 133200695463542130417529127126433675
并查集 547684685
拓扑排序 399207210
有限状态自动机 65468

十一. 二分查找

题目分类 题目编号
二分查找应用(简单) 3743527836769441
二分查找应用(中等) 345402754363003546581624
二分查找与旋转数组 1531543381
二分查找与矩阵 74240
二分答案法 378668410483

十二. 二进制运算的应用

题目分类 题目编号
异或的应用 89136137260268
与或非的应用 371318201

十三. 动态规划

题目分类 题目编号
数组中的动态规划 5097033845551982136509163955212318830932264313403
子数组、子序列中的动态规划 689413446368416279
背包问题 322518474494377
矩阵中的动态规划 62636412057668822162917496329
动态规划与字符串匹配 58372971155161321311391405141044
状态压缩动态规划 464691698638473
区间中的动态规划 486664375312546
树形dp 337124
数位dp 233600

十四. 数据结构

题目分类 题目编号
数据结构设计——栈与队列 225232284622641155
数据结构设计——哈希表 676355380381
数据结构设计——哈希与双向链表 432146460
前缀树 208211648386677472421212336440
23373378632347692502630407295480
树状数组 307315493327673
线段树 699
平衡树(set/map) 352218363

十五. 采样

题目分类 题目编号
按权值采样 528497
蓄水池抽样 382398
拒绝采样 470478519

十六. 计算几何

题目分类 题目编号
计算几何基础 593447223149
分类讨论法 335
凸包 587
覆盖问题 391

十七. 常用技巧与算法

题目分类 题目编号
博弈论 292
分块 239164
倍增法 330
拓展欧几里得算法 365
洗牌算法 384
找规律 390672
分治法 395667
排序算法 147148
线性筛 204
摩尔投票法 229

结语

希望以上的分类及顺序能帮到大家,未来也有做各分类的题解的打算,欢迎一起讨论。


油猴脚本, 能把所有题目替换为超链接, 曾经发过被吞了, 不知道还有没有人需要

https://pastebin.com/daaaFm4D

password: vyCPqG1cVc

来自 https://leetcode-cn.com/circle/article/48kq9d/

4 / 5 ( 8 votes )

《奇思妙想: 15位计算机天才及其重大发现》第一部分

2021年4月5日 20:39

奇思妙想: 15位计算机天才及其重大发现 第一部分

奇思妙想 第一部分

第一部分 语言大师:如何与机器对话

奇思妙想 封面-极客中心

奇思妙想 封面-极客中心

假设现在第二次世界大战刚结束不久,而你是一位总工程师,负责一项计算机制造计划。你收集了一堆电线和开关,任务是构建出一台机器,以便预测飞机在空中的飞行模式。鉴于当时的技术水平,你只能使用一些不可靠的部件,而且构建的机器要便于装配。这台机器消耗的电力足以供应一个小型工厂。此外你还为它配备了一个足以容纳几千个字符的存储器、加法器、乘法器,以及一套用于计算的指令集。
指令是最不需要操心的问题,它们只需表达清楚,能够完成任务即可。
为了提高效率,你需要让各种指令尽可能与工程设计的细节相适应。这台机器的存储器具有不同的运行速度和能耗,因此你需要设定一个指令组用于在慢速存储器和快速存储器之间传递数据,设定另一个指令组用于在快速存储器中对数据进行四则算术运算或其他操作。这种分组方式无疑不利于编写预测飞行模式的程序,如果将存储器视为一种不分类的资源来进行指令分组,应该会容易不少,但你并不为此担心。在当时,一毛钱就能雇来一打学数学的编程人员,而相比之下,机器本身却非常精致、昂贵且罕见。
五年之后,情况发生了根本性的变化。你吃惊地发现,随着对程序的需求高涨以及硬件成本的降低,程序设计的费用要远高于硬件的购买和维护成本。(这种态势一直延续了下来。到了20世纪90年代中期,在大多数企业的软件开发环节中,程序设计的开支已经达到了硬件开支的50倍左右。)因此你为自己制定了两个新的目标:缩短程序第一个版本发布所需要的时间,以及让程序更易于改进,以适应客户需求的变化。
你决定修改编程人员与机器沟通所使用的语言。新的语言应当反映出所要解决问题的结构,而不再是反映存储器的快慢层级和算术运算。每一种行业都会根据自身的目的而发明某种语言,例如电影导演会用“ 开拍”(action)这个简单的术语来表示“ 摄像师开始拍摄,演员开始表演,其他人员各就各位”。由于在20世纪50年代中期,计算机主要应用于纯科学和数据处理,因此你更关注的是创造一种数学语言,包括运算公式、求和以及向量数组。为了开拓思路,你拜访了许多研究人员,也了解了许多项目,它们允许程序员输入类似(X + Y)/Z这样的数学表达式,然后计算机就能自行编译并且进行相应的运算。
到了1958年,有一门科学编程语言脱颖而出,它就是约翰·巴科斯 ① 和他在IBM公司的同事合作设计的Fortran(取自formula translation的缩写,即公式翻译)。这门语言至今仍然是自然科学界使用最为广泛的编程语言。

① 约翰·巴科斯(John Warner Backus,1924—2007)被称为Fortran语言之父,1977年获得图灵奖。本书第1章详细介绍了他的生平。

与此同时,一个研究团队在1956年开辟了计算机的一个崭新领域:人工智能(artificial intelligence)。这门学科的一个目标是创造出具备人类推理能力的计算机。由于人类大部分的推理过程都需要进行抽象(例如积木的相对位置、句子的语法、不同洗衣皂的优缺点),偏重数学运算的Fortran语言对此并不适用。卡内基-梅隆大学的艾伦·纽厄尔 ① 和赫伯特·西蒙 ② 建议用符号表(list of symbols)来表现抽象概念,并且把推理过程视为一种“ 符号操作”(symbol manipulation)。
假设有一块红色积木、一块蓝色积木和一块黄色积木。一个5岁大的小孩也可以告诉我们,如果红色积木在蓝色积木之上,而黄色积木又在红色积木之上,那么黄色积木必然也在蓝色积木之上。将前两个事实用符号表来表示,我们可能会写:(above redblock blueblock)以及(above yellowblock blueblock)。之后引用符号操作规则,我们便能得出:如果(above x y)和(above z x),则(above z y)。
年轻的约翰·麦卡锡 ① 对这一想法印象深刻,但他认为纽厄尔和西蒙的设计过于复杂。他简化了设计,加入了两个强大的功能,也就是我们所知的递归(recursion)和求值(eval),并且创造了一门称为Lisp的语言(取自“ list processing” 的缩写,即表处理)。Lisp不仅是人工智能方面的首选语言,且自1958年诞生起,和Fortran一起不断为计算机语言的设计提供灵感。
这一年的夏天,一个国际计算机科学家小组在苏黎世成立,开始设计纳入Lisp元素的Fortran语言的继任者。他们的精诚合作产生了里程碑式的Algol 60,它直接催生了Pascal、C和Ada这些语言,同时也是从PostScript到Excel等各种语言的直系祖先。

① 艾伦·纽厄尔(Allen Newell,1927—1992)专于计算机科学和认知信息学领域,是信息处理语言(IPL)的发明者之一。1975年他和赫伯特·西蒙因人工智能方面的基础贡献而一同被授予图灵奖
② 赫伯特·西蒙(Herbert Alexander Simon,中文名为司马贺,1916—2001)是现代一些重要学术领域,如人工智能、信息处理、决策制定、注意力经济、组织行为学、复杂系统等的创建人之一,并且获得了很多荣誉,如1975年的图灵奖、1978年的诺贝尔经济学奖以及1986年的美国国家科学奖章。

然而到了20世纪70年代早期,一些研究人员逐渐发现由Fortran、Algol和Lisp所衍生出来的上百种语言都存在着一个共同的弱点:写出来的程序让人很难读懂。在实际工作中,人们发现如果想修改程序的行为,往往都需要整个重写。
拥有生物学背景的艾伦·凯 ② 认为应该把程序构建成像活的生物那样:由独立自主的“ 细胞” 构成,彼此通过讯息传递进行合作。他认为通过自主的单元来构建程序,将会使这些单元能够融入新的上下文情境。对于这种“ 细胞”,他使用了“ 对象”(object)一词来称呼,并且将这种方式称为“ 面向对象”(object-orientation)。对象的行为取决于它得到的讯息。
比如说,在与图片有关的程序中,可以用“ 旋转30度”、“ 开始打印” 这样的讯息或命令来描述常用行为。而如果是操控登月机器人的程序,则可能是“ 收集岩石样本” 或者“ 找到高度超过1米的石块” 。20世纪70年代中期,艾伦·凯带领他在施乐公司的团队设计出了首个面向对象的语言Smalltalk。大批年轻人在数年间加入了它的用户群。由此开始,Smalltalk深刻地影响了C++等日趋成熟的编程语言,其自身也衍生了一批专业的追随者。
过去四十年间,世界上诞生了成百上千种编程语言,约有60种在今天仍然被广泛使用,其中包括文字处理、电子表格、图形设计和动画设计等领域的专用语言。就像工具帮助木匠解决问题一样,一门好的编程语言也能帮助我们解决计算机的问题。而且编程人员也正如木匠习惯于自己的工具那样习惯于自己钟爱的语言。

① 约翰·麦卡锡(John McCarthy,1927—2011)是人工智能方面的鼻祖,1971年获得图灵奖。参见本书第2章。
② 艾伦·C.凯(Alan Curtis Kay,1940-)在面向对象编程和窗口式图形用户界面方面作出了先驱性的贡献。2003年获得图灵奖。参见本书第3章。

语言学家本杰明·沃尔夫 ① 曾经说过:“ 语言塑造了我们思考的方式,决定了我们思考的内容。” 他这番话是针对人类语言所言,而计算机语言则进一步印证了这一主张,尤其是那些灵感来源于巴科斯、麦卡锡和凯的成果的计算机语言。

① 本杰明·沃尔夫(Benjamin Lee Whorf,1897—1941)是美国人类语言学的重要人物。1931年于耶鲁大学师从人类语言学家萨丕尔,提出了“ 萨丕尔-沃尔夫假说”,创立了“ 语言决定论” 和“ 语言相对论”,认为语言影响并制约着人类的思维模式。

1、约翰·巴科斯 不断进取的发明家

我们不知道自己想要什么,也不知道该怎样去做。一切都是顺其自然地发生。我们面临的第一个挑战就是不知道这种语言看上去应该是什么样子。然后就是怎样解析表达式——这是个大问题,而且在今天来看,我们当时的做法非常笨拙……

——约翰·巴科斯谈及Fortran的发明过程

常言道,需要乃发明之母。不过对有些人来说,发明的动力并非来自需要,而纯粹只是因为受不了事物的混乱和低效。约翰·巴科斯就是这样一位发明家。他开启了三个伟大的发明:第一门高级编程语言Fortran、为高级语言描述语法规则的巴科斯诺尔范式,以及函数式编程语言FP。
他的这三大贡献至今仍推动着世界上的科研和商务的发展。然而对巴科斯自己来说,这些发明之所以诞生,只是由于他对当时的概念工具感到不耐烦。
对效率低下的厌恶似乎来自于家庭的遗传。第一次世界大战前,巴科斯的父亲从阿特拉斯火药公司的一个小职员被提拔为首席化验师,这家公司主要生产用于炸药的硝化甘油。他的晋升有着充足的理由。

他们的工厂产量很低,而且经常会发生爆炸事故,却一直无法找出原因。产品的生产对作业温度极其敏感。我的父亲发现他们的高价德国温度计不准确。于是他前往德国,学习了温度计的制作技术,并且生产出了一批高质量的温度计。之后,工厂的爆炸事故明显少多了。
第一次世界大战期间,老巴科斯应征入伍成为一名军需官。战后他并未得到政府允诺的杜邦公司 ① 的职位,于是转行做了一名证券经纪人。到1924年约翰·巴科斯在费城出生时,他的父亲已经在一战后的经济繁荣中略有发迹。巴科斯在特拉华州威明顿市度过了他的童年时光,之后在宾夕法尼亚州波茨敦市就读于著名的希尔中学 ② 。
我每年的考试都不及格,从来就没认真过。我讨厌学习,成天游手好闲。为此我不得不每年都参加新罕布什尔州的暑期补习班。我其实很高兴,因为在那里我可以驾驶帆船,玩得非常痛快。
1942年巴科斯好不容易从希尔中学毕业,依照父亲的意愿进入弗吉尼亚大学主修化学。他对理论知识还比较感兴趣,但痛恨实验室。大部分时间,巴科斯都忙于参加各种派对,坐等年龄一到就去服兵役。到第二学期结束为止,他每周定期参加的课程只有一门不计学分的音乐鉴赏。最后他终于引起了校方的注意,弗吉尼亚大学的生涯也随之结束。随后他于1943年入伍。

① 杜邦公司(DuPont)是世界排名第二大的美国化工公司,创办于1802年,早期以制造火药而出名。
② 希尔中学(The Hill School)是一所美国的大学预科学校,1851年建立,十校联盟成员。著名校友还包括美国前国务卿贝克。

巴科斯成为了一名下士,在佐治亚州的斯图尔特堡带领一队防空兵。不久他在一次能力测试中表现优异,于是被指派前往匹兹堡大学攻读工程学预科。而之后的一次医学能力测试可以说是救了他的命。
战友们被装船参加阿登战役 ① ,而我则跑到哈弗福特学院念医学预科。
作为医学院预科的一部分,巴科斯在亚特兰大一家医院的神经外科病房实习,治疗头部创伤。
一次意外的巧合,巴科斯被诊断出患有骨肿瘤,在颅内安装了一块金属板。不久之后,他就读于花与第五大道医院的医学专科学校(现在的纽约医学院),但只坚持了九个月。
我受够了。医学专科学校的人不喜欢思考。他们只会死记硬背,而且也要求你这样做。在那里根本不允许你思考。
在此期间,巴科斯感觉颅内的那块金属板不太合适,于是到附近专门研制金属板的斯塔顿岛医院,希望能更换一个。由于对医院提出的设计方案不满意,他掌握了相关技术后自己设计了一款。在此之后,巴科斯退出了医学领域,在纽约找了一间小公寓住下来,每个月租金18美元。
当时我真的不知道这辈子到底想要干些什么。因为我喜欢音乐,所以想要搞一套高保真音响,但在当时并没有这种设备,于是我上了一所无线电技术学校。在那里我遇到了一位非常好的老师,也是我遇到的第一位好老师,他要我与他合作,帮某个杂志做些电路特性的计算工作。

① 阿登战役也称突出部战役(Battle of the Bulge),是第二次世界大战期间纳粹德国1944年西线最大的阵地反击战,双方投入近60个师,德军和盟军各伤亡8万余人,其中7.7万人为美军,被称为第二次世界大战期间美军最血腥的一次战役。

我记得做了一些相对简单的运算,得出了放大器电路曲线上的几个关键点。工作非常艰苦、乏味,但是它引发了我对数学的兴趣。它能付诸真正的应用,正是这一点吸引了我。
巴科斯进入了哥伦比亚大学的基础教育学院,开始读一些数学课程。他不喜欢微积分,但对代数很感兴趣。到了1949年春,25岁的巴科斯还有几个月就将拿到数学学士学位,但仍然对自己的前途一无所知。
就在这个春天的一天,巴科斯参观了位于麦迪逊大街的IBM计算机中心。当时他参观了可选循序电子计算器(Selective Sequence Electronic Calculator,SSEC),这是一台IBM早期基于电子真空管制造的机器。
SSEC体积非常庞大,占据了很大一个房间,其上密布着仪表和线路。在参观中,巴科斯向带他参观的人提出自己想找一份工作,而她让他去找主管谈。
我说算了,我没法去。当时我看上去很邋遢,头发也很乱。但是她坚持让我试试,所以我还是去了。他们让我参加一个测验,我做得还可以。
巴科斯受聘在SSEC上工作。这台机器实际上并不是现代意义上的计算机。它没有储存软件的存储器,因此程序每次都得靠穿孔纸带“ 喂” 进去。SSEC有着成千上万个电子机械部件,运行并不是很可靠。
在那台机器上工作很有乐趣。你可以独自使用整个机器。你必须时刻就位,因为那玩意儿每三分钟就会出错,然后停止运行。你必须想办法让它重新启动。
编程的情况也很原始。
一份操作说明和一份指令清单,这就是你能做的编程。要想完成一件事情,每个人都得自己想办法。而完成事情的方法有无数种,所以人们自然会用各种各样的方式来编程。
巴科斯在SSEC上工作了三年。他的首个大项目是月球历表的计算,即计算在任一给定时刻月球的位置。在当时,IBM有能力供养一个纯科学部门,他们曾与哥伦比亚大学合作一个项目,想办法将穿孔卡和纸带机用于科学研究。公司的收入来源主要是为企业和政府生产纸带机。夺人眼球的庞然大物SSEC只是为这种技术起到些宣传作用,而技术本身最终还是被IBM淘汰了。不管公司当初是出于什么动机,巴科斯毕竟从SSEC的工作中学到了很多东西。同时他也为科学计算作出了自己的第一次贡献:在小型机上做多位数运算的痛苦,迫使他开发了Speedcoding程序。

Speedcoding:用小字长表示大数值

在计算机领域,作为整体进行四则运算的一串数码单位称为“字”(word)。这些字的长度都是固定的,根据硬件不同,通常能包含8到32位数字。由于这种长度的限制,必须采用特别的手段才能表现出以埃(千万分之一毫米)或者光年为单位的数值。
著名数学家约翰·冯·诺依曼 ① 是科学计算领域的先驱之一。早在匈牙利的童年时期,冯·诺依曼就被视为数学天才,高中尚未毕业就发表了自己的首篇数学论文。他的记忆力也同样惊人,曾一句不漏地复述出整部《剑桥世界古代史》,而这只是为了取乐。他一生的科研成就包括古典数学和量子力学方面的基础理论,以及提出了博弈论。1933年冯·诺依曼移居美国,进入了普林斯顿大学高等研究院,并且在第二次世界大战期间的曼哈顿计划 ① 中承担了领导作用。

① 约翰·冯·诺依曼(John Von Neumann,1903—1957),美籍匈牙利人,现代电子计算机创始人之一,被称为计算机之父。他在计算机科学、经济学、量子力学及几乎所有数学领域都作出过重大贡献。1931年以不到30岁的年纪成为普林斯顿大学的终身教授,1933年转入该校高等研究院,与爱因斯坦等人一起为最初的六位教授。1994年被追授美国国家基础科学奖。

冯·诺依曼意识到,在原子弹的设计过程中需要涉及非常大和非常小的数字的运算,而宾夕法尼亚大学莫尔电机工程学院(Moore School)的一个电子计算器项目引起了他的兴趣。1944年他加入了这个项目,最初作为观察员,之后便开始亲身参与,并且大力提倡在计算机中既存储指令又存储数据的方法。这种方法在今天早已应用于所有的计算设备 ② 。
第二次世界大战结束后,冯·诺依曼研制了自己的计算机(名为“ 约翰尼克”),并且设计了一批早期程序,用于解答核物理中一些无法通过手工解决的问题。出于对科学计算的浓厚兴趣,他建议在计算机中使用一种“ 换算因子”(scaling factor)来存储和操作那些非常大或者非常小的数字。

① 曼哈顿计划(Manhattan Project)是美国陆军部自1942年起开始实施的核武器研究计划。该工程集中了当时西方国家(除纳粹德国外)最优秀的科学家,动员人数超过10万,历时3年、耗资20亿美元,于1945年成功地进行了世界上第一次核爆炸,并按计划制造出两颗实用的原子弹。整个工程取得圆满成功,但也引发了针对科学家道德的争论。
② 提倡并不等同于发明。冯·诺依曼将此归功于阿兰·图灵,其设想的“ 机器” 也能存储指令。而电子存储器的真正构建则应归功于莫尔学院的约翰·艾克特(John Eckert),他利用水银延迟线原理,用充满水银的振动管来存储数据,从而首开先河。——原书注

原理其实很简单。假设一个计算机字只能存储3位数字,那么要想表示数字517,就需要向计算机字中写入517,并且指定其换算因子为0。要想表示51.7,则同样写入517,然后指定换算因子为-1。表示5170则指定换算因子为1,表示5 170 000则指定换算因子为4,依次类推。正值的换算因子等于跟在数字后面的0的个数,而负值的换算因子则等于小数点后的数字个数。
在进行运算时,编程人员也许并不知道每次计算的精确结果,但他们应该能知道它的换算因子是多少——至少冯·诺依曼认为是这样。没错,原理确实很简单,但前提是你碰巧还是一个优秀的数学家。巴科斯从程序员的角度出发,对此提出了自己的看法。
这样的结果是,你必须对问题了如指掌,知道其中各种换算因子,否则数字就会溢出,或者因为取整时误差太大而出现错误。在这样一种计算机条件下,编程工作变得非常复杂。
与IBM的同事哈伦·海尔里克(Harlan Herrick)一起,巴科斯开发了一款名为Speedcoding的程序,利用浮点数来支持运算。浮点数自身带有换算因子,从而为编程人员卸下了重担。巴科斯在Speedcoding积累的经验为他日后迎接更大的挑战奠定了基础。
当时每个人都知道编程的费用是多么昂贵。租赁机器需要数百万美元,而编程的成本与之相比只多不少。编程费用居高不下是因为你必须雇佣很多人力用“ 汇编” 或者第二代语言写程序,这些语言与满是 0 和 1 的机器码或者说二进制系统相比,进步非常有限。汇编语言相当浪费时间,只为一台机器写命令就要费九牛二虎之力,然后还必须校正由此所产生的大批错误。在这种混乱拖沓中往往会丢失程序本来的目标。

Fortran:第一门高级计算机语言

1953年12月,巴科斯向当时他在IBM的老板卡斯伯特·赫德(Cuthbert Hurd)提交了一份备忘录,建议为IBM 704机设计一种编程语言(该机器当时已经具备浮点运算能力)。这个项目后来被称为“ 公式翻译”,也就是Fortran。它的目标非常明确。
这种语言只是为了大幅度缩短编程的时间。我并没有想过会在别的机器上使用它,当时也几乎没有什么别的机器。
但首先,巴科斯需要面对时任IBM公司顾问的冯·诺依曼的强烈反对。
他并不认为编程存在多大的问题。我认为他当时反对的主要理由之一是,在浮点运算中你很难知道自己会得到什么。而如果保持定点(fixed point),你起码在出现问题时知道问题出在哪里。但是他没有考虑到编程成本的问题。他认为 Fortran 简直就是不切实际、白费力气。
最终赫德还是批准了这项计划,冯·诺依曼也不再坚持。巴科斯招募的团队兼收并蓄、不拘一格,既有经验丰富的程序员,也有初出茅庐的数学系毕业生。1954年秋天,他的程序研究小组已经有了一个清晰的目标:为IBM 704打造一款能让编程更加容易的语言。
正如他们所见,设计语言并不是最困难的部分。真正的挑战在于如何翻译语言,让机器能够直接理解。进行这项翻译任务的程序被称为“ 编译器”(compiler)。今天的大学生可以在一学期之内就设计并实现出一个新的语言编译器,但在当时,巴科斯和他的团队缺少这种算法。尤其是对编译器的核心部分“ 语法分析器”(parser)的设计,他们一直止步不前。
计算机的语法分析和我们在初中学过的语句分析很类似(可能大部分人一毕业就都丢在脑后了)。在语句分析中,我们需要找出不同部分之间的关系,例如限定词、名词、动词、形容词和副词,然后用树形结构图表现出来(参见图1-1)。

奇思妙想 巴斯德范式1 - 极客中心

图1-1 分析一个英语句子

对于编译器而言,则是由语法分析器先画出树形结构图,再将“ 高级语言”(人类易于理解的语言)翻译成“ 机器语言”,以一系列指令的形式直接输入到计算机的电路中(不同模式的计算机往往支持不同的指令,这也是Windows系统的计算机上不能运行Mac程序的原因)。
机器语言程序的效率取决于设计人员使用那些存储速度高但存储时间短的寄存器(register)的效率。现代计算机最少有16个寄存器,有些则达到几千个。与寄存器相比,随机存取存储器RAM(Random Access Memory)可以储存数百万字节的海量信息,但由于其存储速度较慢,绝大部分计算行为仍然发生在寄存器中。

比如说,(A + B)/C这样一个数学表达式可能会被翻译成以下指令序列:
(1) 复制A的值到寄存器1
(2) 复制B的值到寄存器2
(3) 复制C的值到寄存器3
(4) 将寄存器1和寄存器2的值相加,并将结果放入寄存器1
(5) 用寄存器3除寄存器1的值,并将结果放入寄存器1

在这个例子中,算术运算需要将数据放入寄存器中(有些机器不需要将数据放入寄存器,但
相应的运算也会慢很多)。如果在下一步运算中,除了变量A、B、C外还需要D和E,那么编译器
就要决定是将D和E的值放入新的寄存器4和5,还是再次使用之前的1、2和3。这个决定又取决于
A、B和C将何时被再次用到,以及其他很多因素。这个问题非常复杂,至今也尚未完全得到解决。
巴科斯和他的团队属于首批寻求合理方案的人。
而最大的挑战在于Fortran的新功能: DO 循环语句。循环语句能让程序员更加方便地执行重复
性计算。比如说以下指令序列:

DO 13 J = 1, 100
C[J] = A[J] + B[J]
13 CONTINUE

该指令将A的第一个元素和B的第一个元素相加,得到C的第一个元素,然后将A的第二个元素和B的第二个元素相加,得到C的第二个元素,依次类推。

循环的贵族发明者

巴科斯和其他20世纪50年代的从业计算机科学家们可能不知道的是,循环这个概念早在100多年前就已经诞生了。1833年,诗人拜伦勋爵的女儿奥古斯塔·爱达① 遇到了查尔斯·巴贝奇 ② ,当时他正在设计一种名为“分析机”(Analytical Engine)的机械式计算机器。爱达在8岁时就展现了极佳的数学天赋,是当时极少数能够理解巴贝奇高瞻远瞩思想的人之一。二人展开了维多利亚式的终身合作关系,这也让爱达成为世界上当之无愧的第一位计算机程序员。在为分析机设计程序的过程中,爱达根据需要提出了循环和子程序的概念。
爱达整理出了一篇描述分析机的笔记,但拒绝以自己的名字发表,因为她认为女性不应当撰写科学论文。在巴贝奇和她的丈夫洛夫莱斯伯爵(the Earl of Lovelace)的敦促下,她才同意用自己的姓名缩写A.A.L署名。爱达的一生以悲剧结束:她变成了一个赌徒、酒鬼、瘾君子,最后死于癌症,享年36岁。完全是拜伦笔下的那种不幸结局。

要想有效地编译DO语句,需要用到一种特殊的“ 变址寄存器”(index register)。Fortran最初是为了IBM 704设计的,而IBM 704上只有3个变址寄存器,因此属于非常宝贵的资源。
哈伦·海尔里克一发明 DO 语句,我们就知道会有问题——如何实现?我们只有 3个变址存储器,但却需要面对许多下标(subscript)。(上例中的 J 就是一个下标,较复杂的程序可能会包含 20 个以上的下标。)
在程序里,我们很难去判断哪些信息应该使用哪一个变址存储器。如果只用常规方法,得到的代码会很糟糕。我们明白必须去分析代码出现的频率以及其他种种因素。
巴科斯对效率的关注带动了团队的所有成员。Fortran的设计者们很清楚,如果编译得到的机器语言还不如人们手写的效率高,编译就失去了价值,这门新的高级语言就会乏人问津。出于这种原因,他们把将近一半的工作重心都直接放在了如何生成高效的代码上。此举让Fortran一直因为良好的性能而广受赞誉。

① 奥古斯塔·爱达·拜伦(Augusta Ada Byron,1815—1852)是英国诗人拜伦之女,母亲是数学家安娜·伊莎贝拉(Anne Isabella Milbanke,1792—1860)。婚后成为洛夫莱斯伯爵夫人。爱达翻译了查尔斯·巴贝奇早期的程序设计著作《分析机概论》(analytical engine),她被公认为是世界上第一位计算机程序员。1980年美国国防部开发的编程语言就以Ada为名,纪念这位先驱。
② 查尔斯·巴贝奇(Charles Babbage,1791—1871)是英国文学家、数学家、哲学家、机械工程师,计算机概念的第一奠基人。著有世界上第一部关于计算机程序的专著。他发明的分析机是现代电子计算机的雏型。

IBM 704机总共只有大概80位用户,包括通用电气公司、联合飞机制造公司、洛克希德公司等一些飞机制造行业的厂商。1957年4月,西屋电气公司成为了Fortran的第一个商业用户。巴科斯的团队为他们送过去一套存有语言编译器的穿孔卡片。
他们意识到这就是全套 Fortran,没有看任何说明就让它运行起来。当时他们在研究流体力学——计算飞机机翼结构的承压性能等,用于新型飞机的设计。在此之前,他们只能通过大型计算器或者风洞来进行研究。
Fortran的第一次应用没有遇到任何问题。但不久之后西屋公司的科学家和其他一些人发现Fortran编译器中存在着大量错误。巴科斯团队在随后的6个月内修正了这些错误。
巴科斯领导他的精英团队工作了4年,进行了各种复杂的尝试,耗费了大量的精力,但他始终对自己在其中的贡献保持极度的谦逊。
让我独自获得如此多的荣誉,可以说对其他成员很不公平。还有罗伯特·纳尔逊、哈伦·海尔里克、洛伊丝·海贝特、罗伊·纳特、艾文·齐洛尔、谢尔登·贝斯特、大卫·赛尔、理查德·戈德堡、彼得·谢里登。是他们创造了大量的成果。管理这样一个团队并不困难,大家干得都很愉快,我的主要职责就是每天下午两点钟提醒大家结束午餐时的象棋比赛,让他们继续工作。

在发布40多年后,Fortran依然是科学计算的首选语言之一。这门语言至今仍在不断改进的事实就是最好的证明。例如在1992年,一个国际标准委员会给Fortran加入了一个新特性,程序员能够命令编译器允许多台计算机(如果它们有空的话)共同完成一个 DO 循环语句。不过这些故事说来话长,已经脱离了本书的范围。
20世纪50年代后期,巴科斯停止了自己在Fortran方面的工作。然而他对编程语言的贡献才刚刚开始。
1958年5月,一个由商界、学术界的杰出计算机科学家组成的国际委员会在苏黎世召开会议,目标是改进Fortran,并设计出一种单一的、标准化的计算机语言。他们的成果是一种国际化的代数算法语言(algebraic language),后来被称为Algol。
与Fortran相比,Algol有两个显著的优点。首先,这门新的语言引入了局部变量(local variable)的概念。每一个程序都会命名很多不同的数据元素,例如在前文Fortran的那个例子中,我们命名了元素A、B和C。通常一个程序包含的数据元素很多,因此程序员可能会由于疏忽而导致重复命名,也就是将一个已经存在的名称又指派给了一个新的元素,而这会引发一些恼人的错误。名字太常见的人一定能直观地感受到这种麻烦:账单寄错了地址,信用卡因为错误的原因而被拒绝,或者深夜接到电话结果找错了人,等等。在程序设计领域,在整个程序中一直保持含义不变的名称叫做“ 全局变量”(global),而弄混对象的情况则叫做“ 名称冲突”(name collision)。
避免名称冲突的方式之一是让名称的使用环境局部化。比如说,在约克城提到“ 公爵” 一般指的是约克公爵 ① ,而在新港爵士音乐节上提到“ 公爵”,则很可能指的是艾灵顿公爵 ② 。与之相似,局部变量指的是名称只在某个有限环境下有效的计算机存储单元。在这个环境之外,同样的名称可以指派给其他的存储单元。
Fortran只允许全局命名,而Algol则可以局部命名。除了更加方便之外,局部命名也使得随后约翰·麦卡锡引入的“ 递归” 这种编程形式成为可能。
计算机的递归函数是在一定程度上按照自身来定义的。举一个日常生活中递归定义的例子,假设一个女子对自己母系祖先的定义为:我的母亲是我的母系祖先,而我母亲的所有母系祖先都是我的母系祖先。这个定义初看起来好像是循环的,不过让我们仔细地揣摩一下。
假设安妮是芭芭拉的母亲,芭芭拉是卡罗尔的母亲,卡罗尔是多娜的母亲(我们也可以继续定义尤妮斯、弗洛伦斯,等等)。根据我们的定义,芭芭拉是卡罗尔的母亲,因此她就是卡罗尔的母系祖先。而安妮也是卡罗尔的母系祖先,因为她是芭芭拉的母系祖先(芭芭拉的母亲)。我们已经知道安妮是卡罗尔的母系祖先,那么她必然也是多娜的母系祖先。
通过递归,程序员能够将一个问题拆分成许多个同样的小问题,然后将各个小问题的解答整合到一起。比如说,要想整理一大堆文件,我们可以将它们分成两半,先整理一半,再整理另一半。最后再把它们放在一起。

① 约克公爵(Duke of York)是贵族头衔,通常被授给英国国王的第二个儿子,除非该头衔由一个前任君主的儿子所拥有。
② 艾灵顿公爵(Edward“ Duke” Ellington,1899—1974)是美国著名作曲家、钢琴家,对于爵士音乐的发展影响深远。

从严格的理论意义上来说,递归和局部命名并没有让Algol变得比Fortran更强。但是它们启发了一种新的思考方式,从而在日后产生了深度优先搜索(depth-first search)等算法技巧。我们将会在后面章节中讨论这种算法。
巴科斯喜欢Algol蕴含的那些思想,但同时也意识到,要想清晰地表达它们将会非常困难。
他们只是坐在那里玩文字游戏:说明应该这样,而例子应该那样。这些 Algol 委员会(满是关于术语的徒劳辩论)让人非常烦恼,我发现得做点什么。人们需要学会如何准确地使用 Algol。
为了解决这一问题,巴科斯使用了一种叫做“ 上下文无关文法”(context-free languages)的形式体系。该体系刚刚由语言学家诺姆·乔姆斯基 ① 发明(参见下页的补充内容“ 上下文无关文法与巴科斯-诺尔范式”)。乔姆斯基的发明则来源于埃米尔·波斯特在重写语法方面的成果。
至于巴科斯到底是如何想到这种综合方法的,这个问题一定会让历史学家们忙上一阵。有一点我很困惑。我认为自己研究语法的想法来源于埃米尔·波斯特,因为我曾经在莱姆庄园(Lamb Estate,IBM设在马萨诸塞州哈德逊的智囊机构)上过马丁·戴维斯 ② 的课……所以我想如果需要描述什么东西,直接按波斯特说的那样做就行。但是马丁· 戴维斯告诉我说他很久以后才教过这门课(根据戴维斯的记录是在 1960 年和 1961 年)。

① 诺姆·乔姆斯基(Noam Chomsky,1928—)是麻省理工学院教授,为20世纪理论语言学研究作出了重要贡献。
② 马丁·戴维斯(Martin Davis,1928—)也是计算机科学发展史上的先驱人物,纽约大学名誉教授。

所以我不知道应该怎样解释。对于乔姆斯基我一无所知。我是一个孤陋寡闻的人 ① 。
巴科斯的发明最终成为著名的巴科斯-诺尔范式(Backus-Naur Form,缩写为BNF,其中经历了一连串的偶然)。第一个偶然发生在1959年6月,当时联合国教科文组织将在巴黎召开一次关于Algol的会议,而巴科斯准备了一份有关精确语法的报告,准备在会上发言。
这份报告我完成得太晚了,结果未能被收录进大会的官方报告集。于是我只好自己带了一堆报告到会议上,因此分发的效果并不太理想。但是彼得·诺尔(Peter Naur)读到了它,所以一切都不一样了。

诺尔是一位来自丹麦的数学家。他改进了巴科斯的符号表示法,并用于描述整个Algol语言。当编程语言界开始试用Algol时,诺尔的参考手册被公认为是描述该语言语法的最好参考资料。

上下文无关文法与巴科斯-诺尔范式

为了方便理解巴科斯诺尔范式,我们可以参照以下一些英语短语的描述。其中“名词短语”和“动词短语”这两个术语借用自现代语言学。

句子 →名词短语 动词短语
名词短语 → 冠词 形容词 名词 | 冠词 名词
动词短语 → 动词 名词短语
形容词 → 红色的 | 蓝色的 | 黄色的 | 大的 | 小的 | 聪明的
名词 → 房子 | 女孩 | 男孩
动词 → 喜欢 | 打
限定词 → the | a

竖线符号“|”表示可选。比如说,限定词可以是“the” 也可以是“a”。这种语法告诉我们,“the girl likes the boy”(女孩喜欢男孩)是一个正确的句子,因为“the girl”构成了一个名词短语,而“likes the boy”是一个动词短语。而“a smart house likes the boy”(聪明的房子喜欢男孩)这句话尽管在语义上不合常理,在语法上仍然是正确的。
在编程语言中,巴科斯-诺尔范式也具有这种特性。只要遵从其规则,我们就能得到语法上正确的程序,编译器就能成功地将它翻译成机器语言,并且保留其在高级语言程序中的原有含义。当然,原始的程序仍然可能没有意义。编译器只保证翻译的正确性,与原程序是否正确无关。

从自己的发明中解放程序设计

巴科斯发明了世界上最早和最流行的编程语言之一,并且发展了一种可以描述上千种语言的符号系统。很多人,甚至很多杰出的科学家都可能会满足于这些成就而止步不前。但巴科斯没有。
他甚至并不确定自己是否喜欢这些成果。
当你写完一个 Fortran 程序后,其实并不知道程序到底会怎样运行。你只知道它读取了两个数,然后将它们相乘,然后把值储存起来,然后怎样怎样,最后进行判断,如此等等。但你很难弄清楚它实际上都计算了些什么。你也很难用其他方法进行计算,因为你基本上并不理解程序在做什么事情。
巴科斯的目标是让程序员只需表达出他们“要什么”,而无需知道“ 怎样做”。1977年他荣获图灵奖,并发表了名为“程序设计能否从冯·诺依曼形式中解放出来” 的演讲,向整个计算机科学界提出了自己的观念。

在这里提到冯·诺依曼与他早年间曾反对Fortran的开发并无关系。巴科斯所针对的是冯·诺依曼对计算机的特征阐述,即处理器与存储器相连接,而程序和数据都储存在存储器中。在巴科斯看来,这种特征意味着这样一个基本循环过程:从存储器中调取数据,对其执行一些操作,然后将结果返回存储器。而他认为凡是遵循这种范式的编程语言都必然缺乏透明度。以约翰·麦卡锡的Lisp(主要用于人工智能)和肯尼斯·艾佛森 ① 的APL语言为基础,巴科斯提出了一种叫做FP的语言,其主要目的是用数学函数来构建程序。
“ 一般” 语言(冯·诺依曼式的语言)和类似FP的“ 函数式” 语言的主要差别在于,前者实际上是在直接修改计算机内存,而后者则主要依赖于“ 复合函数”(function composition)。巴科斯在他的图灵奖获奖演说中介绍了FP,并使用了求两个数组的内积作为例子。这是在物理中常用的一种运算。标准的冯·诺依曼式语言大体上会以如下形式来写这个运算:

c := 0
for i := 1 step 1 until n do
c := c + a[i] * b[i]

巴科斯从几个方面批评了这一公式,尤其是以下两点。第一,c不断被修改,因此理解该程序的唯一办法就是理解这种修改其实是把一个新的乘积(product)加在一个运行中的总和(total)上。因此若想理解程序,人们必须能在头脑中执行一遍代码。第二,在公式里定义了动态数组(a和b)及其长度(n)。要想适用于一般的动态数组,这些都需要作为“ 参数” 进行传递——在大多数语言中,这都是一个比较棘手的过程。而在FP中,内积运算可以定义为如下形式:

① 肯尼斯·艾佛森(Kenneth Iverson,1920—2004)1962年开发了APL(A Programming Language),这是一种强大、表达丰富而且简明的编程语言。1979年他因对数学表达式和编程语言理论的贡献而获得图灵奖。

Def Innerproduct = (Insert +)(ApplyToAll *)(Transpose)

这个公式要从右往左理解。“ Transpose” 是将两个动态数组的对应元素配对。在上例中,a[1]应和b[1]配对,a[2]应和b[2]配对,依次类推。“(ApplyToAll *)” 是用每一对的乘积来替换它的值,而“ (Insert +)” 则将这些乘积累加。

巴科斯认为这样做有三个主要的优点。其一,没有隐藏的状态(例如上面程序中的变量c);
其二,它对任意两个同样长度的动态数组都有效,因为它没有定义参数(也就避开了参数传递的问题);其三,这当中没有涉及重复运算,因为在这个叫做“ 复合” 的过程中,每一步操作,或者说每一个“函数”(Transpose、ApplyToAll和Insert)都只针对前一步的结果应用了一次。

具有讽刺意味的是,函数式语言,包括FP,并没有流行起来,而其原因正是Fortran流行的原因:函数式语言的程序很难编译成高效的形式。随着处理器和存储器运转速度的提高,权威人士预测人们对程序效率的关注可能会大大降低。不过这些预测至今尚未实现。
除此之外,FP语言也不适用于许多日常的编程任务,尤其是那些需要经常读取数据、修改数据然后将其返回数据库的任务,例如更新账目明细,等等。像这样的任务天生就适于使用冯·诺依曼范式。
设计一种函数语言然后将其与实际工作结合起来是很难的。每一个试图这样做的人都会遇到各种各样的问题。
就算巴科斯没有解决这个问题,他也漂亮地提出了这个问题。其他计算机科学家将接过他手中的火炬继续前进。1991年退休之后,巴科斯功成身退,离开了计算机科学界乃至整个科学界。

他每天都练习冥想,并阅读克里希那穆提 ① 和伊娃·皮拉卡斯 ② 有关人类内省方面的著作。
大多数科学家之所以成为科学家,是因为他们畏惧生活。在科学中有所成就无疑非常诱人,因为在此过程中无需与人产生冲突,无需应付艰难的人际关系,可以完全按自己的方式生活。在这个近乎纯净的象牙塔里,你可以全力施展自己的才华,而没有任何痛苦。与生活中的困难相比,解决科学问题的困难简直就是微不足道的。
而自我反省则不是一种科学活动:它不可重复,也没有好的理论用以指导你如何去做或者追求什么。通过对自己的反省,你可以真正理解宇宙的奥秘,这是非常奇妙的一件事情。而这是通过任何物理定律都做不到的。

① 克里希那穆提(Jiddu Krishnamurti,1895—1986)被公认为20世纪最伟大的灵性导师。他一生走访全球70多个国家演讲,演讲被辑录成80多本书,并被译为50多种语言,被印度及当代佛家学者认为是龙树菩萨再世。
② 伊娃·皮拉卡斯(Eva Pierrakos)是小说家雅各布·韦士曼之女。她创建了自我转化体系Pathwork,目的是达到人性的纯净化。

2、约翰·麦卡锡不走寻常路的常识逻辑学家

如果希望计算机具有一般的智能,那么其外在结构就必须基于一般的常识和推理。

--约翰·麦卡锡

一个5岁的小女孩在玩一辆塑料玩具卡车,把它推来推去,嘴里模仿着喇叭声。她知道不能在餐桌上玩它,也不能用它去打弟弟的头。去学校之前,她会把卡车放到弟弟够不着的地方。放学回家后,她也知道在原来的地方可以找到自己的玩具车。

引导她的行为和期望的推理非常简单,任何一个同龄的小孩都能理解。但是大多数计算机却不能。计算机的问题一部分在于它缺少一般5岁小孩能从父母那里学到的日常社会知识,例如不能损坏家具,不能伤到自己的弟弟;另一部分在于计算机没有我们的日常推理能力。人类使用的是一种基于经验的常识推测体系,它与常规逻辑不同,因此也与一般计算机程序员的思维不同。常规逻辑使用的是一种被称为"演绎"(deduction)的推理形式。演绎让我们能从"所有失业演员都当了服务生"和"托米是个失业演员"这两个陈述中推断出"托米是个服务生"这一新的陈述。其优点在于它的可靠性--如果前提成立,那么结论一定成立。同时演绎推理也是 "单调的"(monotonic,数学术语,其基本含义为"不变的")。如果你发现了新的事实,但并不与前提相矛盾,那么结论仍然成立。

然而,尽管我们大多数人都在学校学过演绎推理,却很少在实际生活中用到。5岁的小女孩相信自己的卡车还在原来的位置,是因为她把它放在了弟弟够不着的地方。但如果某天她在出门时看到弟弟学会了爬凳子,可能就不会这么有把握了。5岁小孩掌握的常识推理主要依靠基于经验的推测,而这可能会由于新事实的出现而不得不作出非单调的修改。而且并不是只有5岁小孩会如此。

就算是公认的演绎推理大师歇洛克·福尔摩斯,也并不经常用到演绎推理。在关于一匹受伤赛马的冒险故事《银色马》中,福尔摩斯运用其天赋的洞察力得出结论,看门狗没有叫是因为它认识罪犯。我们的侦探确实才智超群,而且推论看来合情合理,最后在故事中也证明是正确的,但他用的却不是演绎推理--狗可能是被麻醉了、戴了口套,或者当时正在野地里追兔子。

程序员知道如何让计算机进行演绎推理,因为计算机能够理解其中涉及的数学。但如果想让计算机进行人类赖以生存的这种推测性的(而又常常是正确的)常识推理,就得发明一种全新的数理逻辑。而这正是约翰·麦卡锡为自己设立的目标之一。

麦卡锡的成名还有其他原因。他发明了人工智能领域的首要语言Lisp(list processing,表处理),而且自其诞生之日起,就为编程语言设计提供了丰饶的思想源泉。同时,作为一名教师和难题设计师,他在密码学和平面性检验等亚学科领域激发了众多计算机科学家的灵感。我们将在拉宾 和陶尔扬 的章节中再行描述。

约翰·麦卡锡1927年出生于波士顿一个共产党积极分子家庭,童年在四处奔波中度过。他的父亲是一名爱尔兰天主教徒,先后做过木匠、渔民和工会组织者,全家一直马不停蹄地奔波,从波士顿搬到纽约,然后又搬到洛杉矶。他的母亲是立陶宛犹太人,最初在联邦通讯社当新闻记者,后来就职于一家共产主义报刊,最后成为了一名社会工作者。麦卡锡早年对科学的兴趣与家庭的政治信仰密不可分。

当时普遍相信技术对人类必将有利无害。我记得还是小孩的时候,曾读过一本书叫做《十万个为什么》,是前苏联作家米·伊林 在20世纪30年代早期写的一套通俗科普

书。在美国好像没有这样的书。有趣的是,十几年前我在报上看到过报道一个特别早熟的中国孩子,而他也读过《十万个为什么》。

麦卡锡认为自己的青少年时期平淡无奇,但事实证明并非如此。在上高三时,他得到了一份加州理工学院的课程目录,上面列出了该校一年级和二年级的微积分课本。他买了这些书,完成了所有的练习题目。这使得他最终在1944年进入加州理工后得以免修头两年的数学课程。

1948年,麦卡锡开始攻读数学系的硕士学位。同年9月他参加了加州理工主办的希克森脑行为机制研讨会,大数学家、计算机设计大师约翰·冯·诺依曼在会上演讲了一篇关于自复制自动机(self-replicating automata)的论文,这是一种可以对自身进行复制的机器。尽管当时的与会人员并没有明确地将机器智能与人类智能联系起来,但冯·诺依曼的讲话却激发了麦卡锡的好奇心。

1949年在普林斯顿大学数学系作博士论文时,麦卡锡首次开始尝试在机器上模拟人类智能。

我把有智能的东西看做是一个有限自动机,与同样是有限自动机的环境相连。我和约翰·冯·诺依曼见了面,他对此非常赞成,敦促我一定要把这篇论文写出来。但最后我并没有写出来,因为我认为它还不够成熟。

"自动机"模拟的是随着时间从一个状态转入另一个状态的机器。比如说,普通的手动变速箱汽车在驾驶员点火启动之后会从"熄火"状态转入"空挡但启动"状态。如果驾驶员挂挡前进则转入"启动且挂一挡"状态。而"交互式自动机"(interacting automaton)则是根据其自身的状态以及它所观察到的其他自动机的状态决定从某个状态转入另一状态。有些自动机是智能的(可看做是自带驾驶员),但并不是必须智能。交互式自动机试图在这两种类型之间建立一种连续性的统一体。

麦卡锡放弃了自己对利用自动机模拟人类智能的首次尝试。但在十几年之后,当他从事情境演算(situational calculus)方面的工作时,关于状态和状态转换的思想将重新浮出水面。

在这段时间中,麦卡锡始终没有放弃制造一台像人类那样智能的机器这一想法。1952年夏,普林斯顿大学的一个研究生杰里·雷纳(Jerry Rayna)向麦卡锡建议,可以找一些对机器智能感兴趣的人去收集一些该领域的文章。麦卡锡找的第一批人就有克劳德·香农 ,"信息论"亦即通信数学理论的发明者。香农的理论最初用于远程通信,后被广泛用于语言学、数学以及计算机科学等领域。

香农不喜欢华而不实的术语堆砌。他整理的卷宗为《自动机研究》(Automata Studies)。而其中收集到的文章让我很失望,里面有关智能的内容并不多。

所以在1955年开始筹备达特茅斯计划时,我希望开门见山,使用了"人工智能"这一术语,目的是让参与者们弄清楚我们是在干什么。

1956年在达特茅斯学院举办的夏季人工智能研讨会是计算机科学史上的一座里程碑。这项涉及10人、耗时2个月的雄心勃勃的研究计划,其目标是"基于'我们能够精确、全面地描述人类智能中的学习等特征,并制造出机器模拟之'这一构想,继续阔步前进"(引自其提案)。

研讨会的四位组织者--麦卡锡、马文·明斯基 (当时还在哈佛大学)、纳撒尼尔·罗切斯特 (IBM的杰出计算机设计师)和香农--向洛克菲勒基金会申请了一笔资金支持,金额在今天看来几乎少得可怜:主要组织者每人1200美元,再加上"外地与会人员的火车票",总共7500美元。

麦卡锡在提案中写到,他将研究语言和智能二者间的关系,希望通过程序使计算机能"进行棋类游戏并完成其他任务"。时隔40年后回忆起这次研讨会时,麦卡锡以他特有的直率形容了自己当时的愿景和期望。

我为这次会议设定的目标完全不切实际,以为经过一个夏天的讨论就能搞定整个项目。我之前从未参与过这种模式的会议,只是略有耳闻。实际上,它和那种以研究国防为名义的军事夏令营没什么区别。

创造一台真正智能的机器是一个极为困难的过程。尽管这次会议在实质上并未解决任何具体问题,但它确立了一些目标和技术方法,使人工智能获得了计算机科学界的承认,成为一个独立的而且最终充满着活力的新兴科研领域。虽然大多数与会者在会后并未继续从事该领域的研究,但另外那少数人中却产生了一批在该领域影响深远的成就。

来自卡内基梅隆大学的艾伦·纽厄尔、赫伯特·西蒙和J. C.肖(J. C. Shaw)描述了他们的第二代信息处理语言(Information Processing Language,IPL 2)。这三位科学家致力于构建一种名为"逻辑理论机"的程序,用于验证基本逻辑和博弈论中的定理。而为了做到这一点,他们设计出IPL 2这种程序语言以便于操作对象符号,例如象棋的棋子或者逻辑变量里的真值。由于这种操作与针对数字的算术运算非常不同,他们提议使用一种所谓的"表结构"。

让我们冒昧地借用《爱丽丝梦游仙境》来说明如何利用表来进行符号处理。假设柴郡猫告诉爱丽丝"不是我疯了,就是帽匠疯了"。我们用C、H、A代表三种观点,分别是柴郡猫疯了、帽匠疯了和爱丽丝疯了。猫之前的那句声明可以用表的形式表示为(or C H)。然后猫又告诉爱丽丝"不是你疯了,就是帽匠疯了"。聪明的爱丽丝会把这个声明和之前的那个一起表示为(and (or C H) (or A H))。最后猫又说"我们三个中只有一个疯了"。也就是说,至少有两个没有疯。爱丽丝可以将其表示为(and (or C H) (or A H) (or (and (not A) (not C)) (and (not A) (not H)) (and (not C) (not H))))。

把这些声明表示成表的形式之后,我们就可以定义一些表操作的规则,例如(and (or X Y) (or Z Y)) = (or (and X Z) Y)。也就是说,如果不是X成立就是Y成立,而且不是Z成立就是Y成立,那么不是X和Z都成立,就是Y成立。应用这样一些规则,我们就能得出结论(and H (not C) (not A))。那么,根据柴郡猫的说法,只有帽匠疯了。

通过表来进行逻辑推理的优点在于,在推理的过程中表可以扩展、收缩和重组。此外,我们可以用同一种形式表示规则和数据。在研讨会大多数与会者们看来,表操作无疑是这次的赢家。达特茅斯会议的另一项成就是马文·明斯基关于构建几何定理证明机的提案。明斯基在纸上试验了几个例子,认为证明几何定理可能是对纽厄尔和西蒙提出的基于规则的方法的一种很好的应用。IBM公司的赫伯特·格林特(Herbert Gelernter)和纳撒尼尔·罗切斯特决定去实现这个程序。格林特日后又开发了一种帮助有机化学家合成新化合物的工具,他的儿子大卫·格林特(David Gelernter)是并行程序设计和医学人工智能领域著名的研究者及设计者。麦卡锡身为这个定理证明项目的顾问,有机会为智能行为编写程序。

格林特和他的助手卡尔·格贝里希(Carl Gerberich)采纳了我的建议,以Fortran为蓝本设计了FLPL--Fortran表处理语言(Fortran List Processing Language)。其中也加入了一些他们自己的想法。

1956年,约翰·巴科斯和他在IBM的团队发布了首个高级编程语言Fortran,将从事数字运算的程序员从为每一台计算机写汇编语言中解放出来。直到今天,Fortran仍然是科学和工程计算中的通用语言。FLPL首次尝试了扩展Fortran的符号操作能力。1958年夏天在IBM工作时,麦卡锡试图用FLPL为自己在高中时常用的代数微分应用写一个表程序,但很快发现需要用到递归条件表达式 ,而Fortran却不支持递归。

如果Fortran支持递归,我就能用FLPL做下去。我甚至也考虑了如何往Fortran中加入递归的问题,但是那样做过于复杂。

事实证明,IBM很快就失去了对人工智能的兴趣。一些客户认为智能机器可能会威胁到他们的工作岗位,因此20世纪60年代初期的IBM市场营销都把计算机说成是非智能的快速运算设备,百依百顺、只按要求行事。

麦卡锡不再纠缠于修补Fortran,而是转头发明了Lisp。纽厄尔、肖和西蒙后来把IPL形容为一种越变越复杂的语言,而麦卡锡则把他的Lisp形容为一种越变越简单的语言。

"Lisp"是"list processing language"(表处理语言)的缩写。确如其名,Lisp中所有的数据都用表来表示。这些表都被包含在圆括号中。比如说,(Robert taught Dennis)可能就是表示"罗伯特教丹尼斯"这个句子的一个表。在这种情况下,顺序是很重要的,因为它指明了是谁在教谁。

而(apple tomato milk)则可能表示一个购物清单。在这种情况下,顺序就不重要了,因为这三种商品可以按任意顺序购买。上述这两个例子中,表都包含了"原子"(atom)作为元素。原子和表不同,是Lisp中最小的符号单位,不包含其他任何组成部分。而表还能够包含(而且一般都会包含)其他表作为组成部分。比如说,(Robert taught(Carol and Dennis))反映了句子的语法结构,其中的圆括号指明卡罗尔和丹尼斯都是动词"教"的对象。再比如,(times 6(plus x y))表示6 ×(x + y)。这里的顺序也很重要,而且圆括号表明x和y是一起的。

通过这种方式,表不仅能够表示构成科学和工程的标准数学结构,还能表示构成语言的语句结构。

从一开始,麦卡锡就拥有一个热情高涨的合作者团队。

当我在1958年秋天回到麻省理工的时候,我和明斯基有了一个大工作室、一台键控打孔机,此外还配备了一名秘书、两个程序员和六个数学专业的研究生。我们是在春天时向杰里·威斯纳(Jerry Wiesner)申请的这些,理由是为我们的人工智能项目做准备。

我们连书面提案都没准备,申请就得到了批准。很幸运,当时麻省理工的电子研究实验室刚刚与美国军方签署了一份无固定目标的双向合作协议,而相应的资源还没有到位。我想这种灵活的资源调配正是美国的人工智能研究起步领先于其他国家的原因之一。纽厄尔-西蒙的研究之所以能进行,也是由于美国空军在当时向兰德公司提供了弹性的支持。

随着工作的深入,麦卡锡希望改进这种语言的表达能力。1959年,为了展示Lisp可以明确地表达任何可计算函数,他加入了一个叫做"求值"(eval)的功能。

"求值"允许程序定义新的函数或者过程(procedure),然后将其作为程序的一部分执行。而大多数语言在执行新函数之前都会强制程序中止运行,并且"重新编译"。由于求值函数可以带动并执行任何函数,它扮演了一种"通用图灵机" 的角色,是其他计算机的通用模拟器。

求值概念具有非常实际的意义。比如说,由于国际金融市场时刻都在变化,股票交易所必须每周7天、每天24小时不停地提供计算服务。如果有人写了一个程序,可以用新的方法分析路透社的股票数据,股票经纪人很可能希望马上就使用它,但又绝不想中断自己机器的使用。求值让这一切成为可能。

Lisp

Lisp的基本操作

典型的Lisp函数和过程要么是把一个表拆开,要么是把几个表合并成一个新表。比如说,"追加"(append)函数可以将两个表首尾相接,从而生成一个新表。如果我们打算用名词和动词短语造句,这可能会很有用,例如现在有两个人物鲍勃和爱丽丝,那么(append (Bob kissed) (Alice))将生成(Bob kissed Alice)这个新表。

另一个有用的函数"倒置"(reverse)可以颠倒表中的元素顺序。例如(reverse (append (Bob kissed) (Alice)))将生成(reverse (Bob kissed Alice)),之后再生成(Alice kissed Bob)。而如果把两个函数互换一下,例如(append (reverse (Bob kissed)) (Alice)),则会生成(append (kissed Bob) (Alice)),之后再生成(kissed Bob Alice)。

不论鲍勃和爱丽丝之间的关系如何,我们能够看出Lisp程序可以由函数构成,而这些函数可以处理并产生新的表。这正是吸引巴科斯发明FP的东西。

Lisp中的递归和求值

麦卡锡把递归作为Lisp处理策略中的核心部分。递归是用操作本身对其进行定义的一种方法,因此程序员可以把问题变简单后再进行定义,从而绕过了循环定义的禁忌。

比如说,如果某个表只包含一个元素,我们可以定义这个表倒置后仍然为它本身。如果这个表包含多个元素,我们可以如下定义它的倒置:倒置除第一个元素以外的所有元素,再在其后追加上第一个元素。这句话看起来有点拗口,我们可以这样说明:把表L中的第一个元素称作是L的头部,表示为(head L),而余下的元素称作L的尾部,表示为(tail L)。根据Lisp的理念,我们就可以写出这样一段程序(语法并不精确):

define (reverse L) as
if L has one element then L
else (append (reverse (tail L)) (list (head L)))

我们可以套用一个具体的例子,而这次变成了三角关系:鲍勃、爱丽丝再加上卡罗尔。那么(reverse (Alice Bob Carol)) = (append ((reverse (Bob Carol)) (Alice))) = (append ((Carol Bob) (Alice))) = (Carol Bob Alice)。

由于函数和过程本身是定义为表的,因此我们也可以用其他函数和过程来构造它们。这样求值函数就可以带动并执行这种函数或过程了。

Lisp中蕴含的思想吸引了负责设计Algol语言(巴科斯和诺尔为其发明了巴科斯 诺尔范式)的国际委员会。1960年,在该委员会于巴黎召开的会议上,麦卡锡正式提出了递归和条件表达式这两个概念。在标记方法上,委员会有了些争论,但最终仍然接受了他的思想。

Algol是第一个采用Lisp创新的语言,但绝对不是最后一个。Algol的后继语言如Pascal、C、Ada以及其他大多数现代编程语言都支持递归和条件表达式。但直到最近,主流语言都不支持求值,主要原因在于语言设计者们担心程序员往运行中的程序里添加新功能可能会很危险。不过如今的很多程序都必须每周7天、每天24小时地连续运行,人们对求值这种特性的需求越来越迫切,因此大多数实验性语言都包含了求值或类似求值的功能。

近50年来,Lisp一直是人工智能领域的标准语言。麦卡锡并未预料到它会有如此长的寿命,甚至曾建议将其修改成类似Algol那样。然而该领域的编程人员仍然喜欢Lisp最初的语法。麦卡锡和在他之前的巴科斯、之后的艾伦·C. 凯一样,最终已无法控制自己发明的语言的发展方向。

让常识合乎逻辑

幸运的是,麦卡锡一直都只是把Lisp看做一种手段,目的是达到他的主要目标(至今也依然如此):制造一台像人那样有智慧的机器。

在他于1959年发表的论文《具有常识的程序》中,麦卡锡详细地阐述了这一目标。以这篇论文为起点,他开始了穷其一生的不懈求索,力求将数学的精确应用到所谓"常识"这一难以捉摸的推理形式之中。

为了具体说明这一目标,他将具有常识的程序定义为"对任意给定的事物(自己已知的或别人告知的),能够独立演绎推理出它将产生的一系列直接结果"。作为例子,他描述了一个人从书桌边离开并驱车前往机场这一事件涉及的推理过程 。

在论文介绍后的讨论环节中,著名的逻辑学家和语言学家耶霍舒亚·巴尔 希勒尔 将麦卡锡的方法形容为"半生不熟的"和"伪哲学的"。他认为麦卡锡所提倡的推理不能被称为是"演绎推理",因为其结论并不总是成立。希勒尔指出:"叫辆出租车去机场不是更便宜吗?难道不能退订这趟航班,或者做点别的事情?"

麦卡锡回应了批评,他同意自己的论文基于"不明确的哲学假定之上。……每当我们设计程序让计算机学习经验时,就是在把某种认识论添加到程序中。"从那时起,解决认识论问题成了麦卡锡和其他少数人工智能理论家研究的重心所在。

在读者了解这些研究之前,也许想知道最开始为什么会有人想把常识纳入到计算机程序中来。这对科学或社会能带来什么好处?麦卡锡给出了答案。

所有的科学以及专业理论中都引入了常识。当你想要改进理论时,你总要回到常识推理,因为是常识推理主导着你的试验。

所以,如果有人想设计出更好的象棋程序,就需要对自己的成果进行试验,让它分析各种棋局。有关进行何种试验的所有推理都立足于常识框架之内。

我们从很多科学家的著作中都能找到类似的观点。例如理查德·费曼 在他著名的物理学讲义中讨论对称时,就提出了如下观点:

我们所有的物理思想,在应用中都需要一定的常识。它们并不只是纯粹的数学的或抽象的思想。当我们说把仪器移动到新的位置,得到的现象相同时,必须正确理解这句话的意思。它的意思是我们移动了所有我们认为相关的东西。如果得到的现象不同,可能是因为有些相关的东西被遗漏了而没有移动,我们就要把它们找出来。

常识推理的主要优点之一在于其应变性。当环境中出现了新情况时,它能够很好地适应。麦卡锡给出了下面的例子。

假设一位旅客要从格拉斯哥经伦敦飞往莫斯科。再假定他知道自己得购买机票,也知道整个旅程的目的地顺序。

很多程序都能够进行如下推理:如果他从格拉斯哥飞到伦敦,再从伦敦飞到莫斯科,那么他就会在莫斯科。但如果他在伦敦把机票丢了怎么办?原计划将不再有效,但如果原计划中包含了再买一张机票的情况,就不会有问题。然而现有的实用程序都不能像这样细致地设置环境条件。

1964年,麦卡锡已是斯坦福大学的人工智能实验室主任。他提出了一种名为"情境演算"的逻辑理论,其中"情境"代表着世界的一个状态。当主体(agent)行动时,情境就会相应发生变化。主体下一步如何行动取决于他对情境的了解。

在麦卡锡的旅行例子中,没有意识到自己丢了机票的旅客会直接前往机场,而意识到自己丢了机票的旅客则可能会去旅行社,找(假设有智能的)代理人进行交涉。

情境演算和有限自动机(finite automata)理论中都有状态转换的概念。但情境演算中的推理不仅取决于情境,同时还取决于主体对情境的了解。主体知道或能够知道得越多,作出的决策就会越正确--而这正是他所希望的。情境演算理论吸引了众多研究人员,他们用各种方式应用这一理论或其变体,但它本身也引起了新的大问题。

在主体众多且互相联系的世界中,与一个主体相关的情境可能会随着其他主体的行为发生改变。当然,在我们的常识世界中,我们知道其他主体的大多数行为在实质上并不会影响我们自己的决策。

比如说,丢三落四的旅客(主体A)的行为不会因为美国总统(主体B)在华盛顿特区晨跑时买了一个小松饼而发生改变。逻辑不会告诉我们这两个行为无关,但是直觉会让我们得出这样的结论(除非你相信那些子虚乌有的阴谋论)。

在与爱丁堡大学的帕特里克·海耶斯 合著的一本书中,麦卡锡用"框架问题"(frame problem)来表示如何简洁地找出不受某特定行为影响的众多事实这一普遍问题。

简而言之,当特定的行为发生时,框架问题不必具体列出所有未变化的事物。

大多数成功的破案都依赖于对框架问题的洞察。英勇的侦探在找到破案线索时,实际上是辨别出了多数人可能会摒弃或忽视的行为和主体之间的联系。例如在《银色马》中,福尔摩斯从看门狗没有叫这一行为确认了狗的主人就是罪犯。而其他人,包括他可敬的伙伴华生医生在内都没有想到这一点。

福尔摩斯在他的调查开始之前作出了如下评论:"对这件案子,思维推理的艺术,应当用来仔细查明事实细节,而不是用来寻找新的证据。这件惨案极不平凡,如此费解,并且与那么多人有切身利害关系,使我们颇费推测、猜想和假设。困难在于,需要把那些确凿的事实--无可争辩的事实--与那些理论家、记者的虚构粉饰之词区别开来。"

计算机所处的环境随时都可能出现新的、意外的事实。要想解决框架问题,它必须像歇洛克·福尔摩斯那样运用一套新的推理方法。1974年马文·明斯基出版了《表征知识的框架》(A Framework for Representing Knowledge,麻省理工学院出版社)一书,主张人工智能不应该使用逻辑,因为逻辑与生俱来就是保守的。

为了理解他的论点,我们假设你听说朋友有一只叫做班卓的鸟。你想象中的情境可能包括班卓会飞。但如果现在你的朋友告诉你,班卓是只企鹅,或者被剪过翅膀,或者腿被绑起来了,或者惧怕飞行,你就不会再认为班卓会飞了。虽然没有违背班卓是只鸟的事实,但新的信息却让你改变了最初的结论。

明斯基把这种推理形式称为是"非单调的"(nonmonotonic)。经典的演绎推理都是单调的--只要和旧的信息不矛盾,新的信息就不会改变原来的结论。日常的推理中,充满了非单调性。我们相信总统先生在华盛顿特区大吃小松饼不会影响到伦敦希斯罗机场的旅客,但如果我们发现当地著名的萨克斯音乐会就以他吃小松饼作为开场信号,我们就会改变主意了。

麦卡锡把非单调性看作是解决框架问题的一种途径。有智慧的主体在进行推理时,从直觉上会先假定遥远的行为不会产生影响;而一旦发现这些行为事实上可能有关,则需要重新推理。与明斯基不同,麦卡锡认为逻辑是解决这个问题唯一可靠的基础。为了做到这一点,逻辑需要一种用于推测的形式化基础。

麦卡锡提出了"限定推理"(circumscription)。这一规则允许主体进行如下推测:"我已经知道了全部可能具有性质P的对象。因此如果遇到新的对象,我会假设它不具有性质P。"

例如对于班卓来说,你假定它会飞。也就是说你针对鸟类限定了"不会飞"的性质。而对于一只叫做强波的大象来说,你限定了"会飞"这一性质,即在没有相反证据的情况下,假定这只大象不会飞--因为它不具备会飞的性质 。因此,限定推理便于我们对事物进行基于经验的猜测。在了解到更多事实之后可能会证明它是错误的,但同时它也使我们能够作出一些有用的假设。

在一个给定的情境中,要想知道应该限定哪些性质,必须先了解世界上的事实。在班卓和强波的例子中,我们必须知道普通的鸟都会飞,而普通的大象都不会。人类几乎不假思索就能运用这些知识,但对计算机而言却是重大的挑战。

比如说,百科全书告诉你拿破仑死于1821年,而威灵顿公爵 死于1852年。拿破仑死时,是英国政府关押在圣赫勒拿岛的囚徒。

但是百科全书本身很可能不会告诉你威灵顿公爵是否听说了拿破仑的死讯。你必须用常识去分析百科的内容才能知道。

你也许会进行如下推理:威灵顿公爵很可能听说了拿破仑的死讯,因为他的戎马一生中有大半都为拿破仑所困扰。因此你可以限定"未听说自己关注的人的死讯"作为性质P。而更简单的问题,拿破仑是否听说了威灵顿公爵的死讯?你可以限定"听说比自己晚死的人的死讯"作为性质P。

最根本的问题在于,如何利用情境的上下文背景来作出自然的推测,例如将军普遍都会关注他们的对手。麦卡锡开始开发一种新的逻辑形式,使其能够处理和利用上下文背景。

其中的部分目标就是要理解人们在说什么。如果AI系统无法利用上下文背景,它就不能理解人们在对话中叙述的内容。

这个问题的难度极大。不仅是机器难以正确地应用上下文背景,就算是人类自身有时也会有困难。已故的前白宫发言人托马斯·奥尼尔 曾讲过一个欢迎新任总统罗纳德·里根 就职的故

事。奥尼尔告诉里根,他有格罗弗·克利夫兰 用过的书桌。里根笑着说他曾经在电影里扮演过克利夫兰。里根说的是棒球运动员格罗弗·克利夫兰·亚历山大 ,而奥尼尔说的则是美国的两任总统。

如此多的问题,如此少的答案

开启了常识推理的潘多拉魔盒 ,促使麦卡锡开始研究一种能允许任意加入新的事实的形式体系。他将其称为"容变能力"(elaboration tolerance)。一旦允许变化,新的事实可能会使推理者在保留部分结论的同时改变之前作出的一些结论。要想得到所有正确和不正确的结论,可能需要用到基于某些预判的可能性得到的限定推测,例如鸟会飞,而大象不会。至于如何判断哪些推测可能成立,这需要知识,以及把知识系统化、整理成上下文背景或微理论(micro theory)的方法。推理者采取行动,知识也会相应增长,因而又会创造出新的事实。这些能力环环相扣、相互依赖。

在1959年发表的论文中,麦卡锡提出其目标是构建一台机器,使其能够"执行一些简单的、任何非低能的人都能进行的文字推理过程"。从字面上看,这一任务似乎并不算难事,但目标依然非常遥远。

读到这里也许有读者会问,麦卡锡试图为人工智能提供逻辑基础,他的众多尝试到底算是成功了,还是失败了?在他自己看来,自情境演算之后他的工作鲜有付诸实践的。医疗诊断或者预测股票价格等专业系统的开发者们,无不想方设法地避免在自己的系统中进行非单调推理或者一般上下文背景推理。麦卡锡和他的同事们揭示出来的困难,如今成了"不得靠近"的警示牌。

但在未来可能会完全不同。如果项目的覆盖面广阔,而又深不见底(就像道格拉斯·莱纳特的Cyc ,试图对亿万事实和规则进行编码),就必将面临许多难以解决的困难。任何这类使用逻辑的项目都必须建立在麦卡锡的工作成果之上(我们在莱纳特一章中将会提到,他的项目运用了R. V. 古哈 提出的"微理论",而古哈则师从麦卡锡)。现有的逻辑工具能否够用?麦卡锡承认他并不知道。

利用逻辑表达世界中的事实的进展一直都很缓慢。亚里士多德没有发明形式体系。莱布尼茨 没有发明命题演算,尽管这种形式体系比他和牛顿同时发明的微积分更加简单。乔治·布尔 发明了命题演算,却没有发明谓词演算。戈特洛布·弗雷格 发明了谓词演算,但从未尝试过将非单调推理形式化。我想我们人类明白,要明确地表征我们思维过程中的各种事实,表面来看似乎简单,实际上是很困难的。

3、艾伦·C·凯清晰的浪漫主义梦想

所有对事物的认识都始自于我们不愿盲目地接受这个世界。

--艾伦·C.凯

长久以来,科学家们一直梦想着机器能够改善人们生活的质量。早在17世纪,哲学家和数学家戈特弗里德·威廉·冯·莱布尼茨就曾设想过一种机器,能通过逻辑推理来平息所有的争论。三个世纪之后的1945年,计算机的雏型、微分分析仪(differential analyzer)的发明人范内瓦·布什 在《大西洋月刊》杂志上发表了一篇名为《诚如所思》 的文章。在这篇文章中,布什预测将来会出现一种叫做"Memex"的设备 ,能够让普通人在资料库中以链接的形式查询、浏览各种微缩胶片,从多方面了解自己感兴趣的主题。科幻作家罗伯特·海因莱因 后来基于他的创意写了一则短篇故事,而这则故事让一个14岁的小男孩着了迷,他就是艾伦·凯。

从那时起,凯开始了他自己充满理想主义的设计发明生涯。他在FLEX机及其后续的Dynabook原型机、编程语言Smalltalk等方面作出的贡献,都极大地影响了现代个人计算机的设计。由他创造出的术语"面向对象"(object-orientation)则是20世纪90年代中期以来编程领域最基本的规范之一。而他与洛杉矶一所公立学校合作的项目,也反映出其自身对于教育体制的不满和反思。

dynaBook - Alan Kay - 极客中心

dynabook - Alan Kay - 极客中心

1940年,艾伦·凯出生于美国麻萨诸塞州的春田市,一年后举家迁至他父亲的故乡澳大利亚。凯的父亲是一位设计假肢的生理学家,母亲则是一位艺术家和音乐家。

我的父亲是科学家,母亲是艺术家,所以在我童年的家庭氛围中充满了各式各样的想法,以及各种各样表达它们的方式。我至今也从未把"艺术"和"科学"分开过。

我的外祖母是一名教师、学者和女权运动者,同时也是麻省大学阿默斯特分校的创始人之一。外祖父是克利夫顿·约翰逊,一位相当有名的插画家、摄影师和作家(出版了100多本书)。此外他还是一位音乐家,弹奏钢琴和管风琴。我出生于他去世的那一年,家里人都认为我是最像他的后代,不管是在兴趣上还是气质上。

1945年初,澳大利亚有可能受到日本的侵略,凯全家又重返美国。1945年到1949年,他们一直住在麻省海德莱市郊的约翰逊农场。凯3岁还在澳洲时就学会了阅读,因此很陶醉于农场的新环境--家里有近6000本书,以及大量的绘画作品。

我记得读过维利·雷 的一本书,名字叫《火箭、导弹和星际旅行》。给我触动很深的一点是,如果你从某个星球要去另一个星球,直着飞过去是不对的。你不能让飞船直接瞄准那颗行星,而应该瞄准它即将运行到的某个地方。

有关星际旅行的憧憬让凯兴奋不已,但学校生活对他来说就很乏味了。

到上学时,我已经读过好几百本书了。我从一年级开始就知道老师们在骗我们,因为我早就从书里知道了其他观点。学校里基本上只允许一种观点:老师的观点或者教科书的观点。他们不喜欢你持有不同的意见,所以就会出现斗争。当然了,我会用自己五岁的声音和他们争论。

凯从他的母亲那里接受了音乐的熏陶。他是学校唱诗班的男高音领唱,十几岁开始弹吉他,二十岁时甚至能以专业吉他手的身份挣钱谋生。他看到了音乐与计算之间的直接联系。

计算机程序有点类似格里高利圣歌 --单声部的旋律不断在乐章中来回变化。并行程序则更类似于复调 。

根据凯的类比,格里高利圣歌的一个乐章会针对某段主旋律进行多个变奏 。而在计算机程序的循环中,同一段指令序列也会被重复多遍,每一次都会以不同的值开始,直到某个"终止值"(stopping value)判断循环结束,之后转入下一个循环,类似于格里高利圣歌的下一段旋律。而在复调音乐中,多个不同的主旋律会同时进行,就像在并行程序中会同时执行多段指令序列。

1949年,凯的父亲获得了纽约一家医院的工作,全家迁往长岛。凯进入了布鲁克林技术高中,纽约市最好的科技高中之一。由于有不服从学校的行为,他被勒令暂时休学,很快又患上了风湿热。他以为必须复读一年,结果发现自己已经拿到了足够的学分,完全可以直接毕业。问题是,下一步怎么办?

我去了西弗吉尼亚州的贝森尼学院念书,主修生物,辅修数学,但后来在1961年因为抗议学校关于犹太人的限额问题被赶了出来。于是我到了丹佛,教了一年的吉他课。然后我被召入伍。当时有这么一项条款,就是如果军队要招募你,你可以自选一个志愿兵种进行申请。

于是我到美国空军待了两年。我爱读书,于是读了军方的所有规章制度,发现我正在接受培训成为士官。而如果我退出了士官培训,就会变成普通士兵,相应的服役期也会从4年变成2年。所以我就退出了那个培训。

在美国空军时,凯通过了一项能力测试,成为了一名程序员,在IBM 1401上工作。这个型号的计算机在当时非常流行,市面上大约有15 000台。

他们需要编程人员。在那个年代,编程是一种低级职业,而且大部分程序员都是女性。我的老板就是女的。他们也招收语言学家……的确是个很有意思的群体。

当时我有个朋友,他负责的部分就是我们今天所说的操作系统(控制计算机的程序)。你要知道,1401的存储器只有8K(8000个字符),所以它的操作系统必须小于1K。结果他做到了,机器能够进行真正复杂的批处理,简直是个奇迹。我协助他工作,所以也了解了一些编程的思想。

新思路的萌芽

要想成为伟大的软件设计者,首先需要有能力把单纯的想法转化为正确、有效的程序,其次要能慧眼识珠,善于发现其他的优秀编程思想的价值。

1961年,凯的工作是为空军解决各个航空训练设备之间数据和过程(procedure)的传输问题。他发现不知哪个程序员想出了一个聪明的办法,就是把数据和相应的过程捆绑到一起发送。通过这种方式,新设备里的程序就可以直接使用过程,而无需了解其中数据文件的格式。如果需要数据方面的信息,可以让过程自己到数据文件里找。这种抛开数据、直接操作过程的想法对凯而言是一个极大的触动,为他日后形成"对象"的概念埋下了伏笔。

离开空军后,凯认为自己应该完成大学学业,于是考入了科罗拉多大学。

科罗拉多的数学系比贝森尼学院要好,但是生物系不行。大部分时间我都泡在博尔德分校的校剧院里写舞台音乐。不论什么专业,任何人都能参与剧院的活动,而一年大概能上演25部作品,很了不起。我根据《霍比特人历险记》 写了一部戏,也参与了其他一些作品。

凯曾认真考虑过自己是否应该转行从事音乐,但最终还是拿到了数学和分子生物学的双学位,于1966年顺利从科罗拉多大学毕业。他又一次面临自己职业生涯的选择问题。

我考虑过医学,但感觉自己没有足够的责任心。现在我还是这么认为。

他还考虑过哲学,但最终放弃了。最后他决定去犹他州立大学念计算机科学系。

关于犹他大学,我只知道它的海拔超过4000英尺,而且有博士点。我很享受山里新鲜的空气。之前我曾想过去威斯康星大学念哲学,幸亏没去成。而科罗拉多的博尔德分校没有博士点。于是我身无分文跑去了犹他。对计算机我还比较感兴趣。

除他之外,计算机科学系还有6个研究生和3位教授,由戴夫·埃文斯(Dave Evans)带领,在此之前他曾经在加州大学伯克利分校负责计算机科学系。

进了犹他读博,在分到办公桌之前你先得读一大堆手稿。它们是关于Sketchpad 的。基本上,如果你理解不了Sketchpad,在犹他就别想挺胸做人。

他们还有一项传统,就是让新来的研究生干最脏最累的活。我的办公桌上摆着一堆磁带和一张纸条,上面写着:"这是UNIVAC 108机上跑的Algol语言。如果它不能运行,就把它弄好。"那其实就是第一版的Simula 。

伊凡·苏泽兰 于1962年提出的"画板"Sketchpad,是有史以来第一个交互式计算机图形系统,而且非常成熟。它提出了主图(master drawing)和例图(instance drawing)的概念。程序员可以在"主图"部分定义各种限制条件。这些条件可以很简单,例如"直线L的端点与某点P之间的距离应为1英寸";也可以很复杂,例如"形状M的弯曲应遵从牛顿定律"。Sketchpad会检查各个例图,然后根据主图的这些限制条件对它们进行修改。

Simula语言是1965年由挪威人克利斯登·奈加特和奥利-约翰·达尔 共同开发的一种语言,它也支持类似主图与例图的概念,只是相关术语有所不同。在这两种语言中,程序员可以定义主图的行为,然后让每个例图都遵从这一行为。关于这些想法,凯进行了很多思考。他开始寻找某种基础构件,支持一种简单、有效的编程风格。

我的灵感就是把这些看做生物学上的细胞。我不确定这一灵感来自何处,但肯定不是在我看到Sketchpad的时候。Simula也没有给我太多启发。

与生物学的类比让凯总结出三大原则。第一,每个"例"细胞都遵从"主"细胞的某些基本行为;第二,每个细胞都能独立运作,它们之间由能透过细胞膜的化学信号进行通信。第三,细胞会分化--根据环境不同,同一个细胞可以变成鼻子的细胞,也可以变成眼睛或者脚趾甲的细胞。在日后凯将会把"主/例"区别、信息传递和分化原则运用到Smalltalk的设计中,但此刻它们还只是一些初步的想法,似乎重要,却尚无用武之地。

FLEX和Dynabook

犹他州立大学有许多项目的资金支持都来自于美国国防部的高级研究规划署(Advanced Research Projects Agency,ARPA),当时可谓是ARPA的黄金时代。1962年至1964年规划署的负责人是J.C.R.利克里德 ,他拥有心理学背景,而且对人机交互领域非常感兴趣。

从1962年到1970年,ARPA赞助了一系列大胆的项目,其中一些只是出于对项目负责人的信心,根本与军事无关。1970年的曼斯菲尔德修正案要求ARPA只赞助与军事相关的项目,规划署的名字也被改为国防高级研究规划署(Defense Advanced Research Projects Agency,DARPA)。1993年联邦政府又将名字改回了ARPA,再次回归亲民方针。

在ARPA的资助下,麻省理工学院林肯实验室的威斯利·克拉克 于1963年开发出首台个人计算机LINC 。这种计算机重达数百磅,与今天的掌上电脑相去甚远,但在当时这种项目只要存在就足以令人兴奋。类似的研究很快跟进,而戴夫·埃文斯对此也有所耳闻。

埃文斯曾经在本迪克斯(Bendix)公司当过副总裁,这是一家干实事的公司。他经常为手下带的学生找一些咨询方面的工作。我去了几个月之后,他也帮我找了一份当地的咨询工作。

当时是1967年,戴夫·埃文期认识的一个家伙正打算设计一种台式机,但他不懂软件。我懂软件,所以这看上去是个绝配。

这个"家伙"就是埃德·基德尔(Ed Cheadle)。这个项目就是FLEX机。

基德尔是个很棒的家伙,典型的得克萨斯大块头,天性乐观。我们俩相处得非常融洽,很快开始讨论应该如何设计这台机器。他希望把它设计成一种能为医生、律师、专家服务的工程工作站。

很快我们就遇上了个人计算机最主要的问题:无法预测用户的需求。一旦所有人都有权使用,你就会出问题。所以我很早就对可扩展语言(用户能够根据自身领域的术语和操作进行修改的计算机语言)产生了兴趣。

兰德公司曾试验过JOSS,这是一种为非计算机用户--这里也就是经济学家--设计的系统。我们的想法是把FLEX做成像JOSS那样。事实也确实如此,FLEX有多个窗口,甚至还有类似图标那样的东西。

尽管FLEX拥有现代个人计算机的一些特征,凯仍然觉得不太满意,部分原因在于它笨重的个头有350磅。1968年夏,他在伊利诺伊大学进行了一次有关FLEX机的演讲。演讲受到了欢迎,而凯却对紧随其后的一次参观记忆犹新。

在那里我们看到了第一块平板显示器。从会议回来后我花了大半个晚上的时间,试图用摩尔定律计算出什么时候我才能把FLEX机装到一块小显示器的背面去。

1965年,物理学家戈登·摩尔 就预测,从基准年1959年开始,集成电路板上的密度会逐年翻番。也就是说电路的尺寸每年都会减半。依次计算,凯发现FLEX的20 000个电路元件在1980年左右可以放入显示器的背面--事实证明这一猜测颇为准确。

1968年秋季,凯拜访了麻省理工人工智能实验室的西蒙·派珀特博士② 。派珀特曾与瑞士儿童心理学家让·皮亚杰 一同工作过,他开发了一种简单的编程语言LOGO,并且提倡教儿童使用这种语言自行编程。凯亲眼看到麻省莱克星顿公立学校的孩子们正在编写真正的程序。这一经历改变了他对个人计算机的看法。

西蒙·派珀特(Seymour Papert,1928—),美国麻省理工学院终身教授,教育信息化奠基人,数学家、计算机科学家、心理学家、教育家,近代人工智能领域的先驱之一。他于1968年基于LISP创立了LOGO编程语言。LOGO是一种解释型语言,它内置一套海龟绘图(Turtle Graphics)系统,通过向海龟发送命令,用户可以直观地学习程序的运行过程,因此很适合于儿童以及数学教学。

我们曾把个人计算机比作私人汽车,而大型机则是公共铁路。

在看到派珀特所做的工作之后,我发现之前的比喻不再成立。孩子们自然不会去开车,我们需要关心的是他们如何识别媒介,掌握文明社会的符号系统。

拿到犹他州立大学的博士学位之后,凯到斯坦福大学人工智能实验室工作了一年(1969年至1970年),教授系统设计。

我对人工智能真的不太感兴趣,部分原因在于它太难了。我的生物学背景告诉我,为人工智能建立一套可行性标准绝不是件容易的事。每次我坐下来思考这个问题,总发现那些令其他人满意的成果无法令我满意。在我看来,他们所做的东西并不能称为是智能。

马文·明斯基是一个例外,他发展出了"心智社会"(society of minds)的想法。明斯基对生物学很感兴趣,而且他越想越觉得从生物学上得到了很多启示。

在斯坦福教书期间,凯开始构思另一种个人计算机--书本般大小,能将用户(特别是孩子们)与世界相连。

1970年7月,施乐公司的首席科学家杰克·高曼(Jack Goldman)说服高层在加州帕洛阿尔托市建立了一个长期科研部门--帕洛阿尔托研究中心(即PARC,Palo Alto Research Center),并邀请ARPA的梦想家鲍勃·泰勒 加盟管理。泰勒许诺凯以极大的自由,让他有机会"听从自己的本能行动"。于是凯加入了施乐PARC,开始设计一种叫做KiddiKomp的笔记本电脑,用一块小电视屏幕(14英寸)作为显示器,并配有可拆卸的键盘和鼠标。

在凯加入之后,泰勒从一家名叫伯克利计算机公司的濒临倒闭的小企业挖来了很多人才,其中包括巴特勒·兰普森 (1993年获得图灵奖)等名人。随后,从斯坦福研究院的增智研究中心(Augmentation Research Center)的道格·恩格尔巴特 带领的开拓性用户界面团队也来了许多研究人员,包括鼠标的合作发明人比尔·英格利什(Bill English)。英格利什鼓励凯申报预算、建立团队。

凯不情不愿地当起了领导者,组建了"学习研究工作组"(Learning Research Group,LRG),开始招募和他一样对笔记本电脑这一想法怀有热情的人。到了1971年夏天,凯开始设计一种新的语言,名字叫做Smalltalk。

这个名字非常不起眼,以至于它的任何成就都会让人感到惊喜。

"面向对象"的诞生

Smalltalk确实与生物学上的类比相吻合:相互独立的个体(细胞)通过发送讯息彼此交流。每一条讯息都包含了数据、发送者地址、接收者地址,以及有关接收者如何对数据实施操作的指令。凯希望这种简单的讯息机制能贯彻到整个语言中去。到了1972年9月,他已经完成了对基本想法的简化,甚至能把Smalltalk的整个定义写在一张纸上。这些想法组成了凯所谓"面向对象"的核心内容,它在20世纪90年代变成了软件设计的基本技巧。

如今,对"面向对象"这一术语过度的滥用几乎导致它失去了自己本来的意义。而它最基础的含义仍然来自类比的对象:独立、相互交流的生物细胞。细胞在特定环境下对特定化学信号产生特定蛋白质。与此类似,计算机对象也会对特定计算机讯息作出特定反应。比如说,一个代表视频游戏的计算机对象,会对敲击键盘、移动鼠标等讯息作出改变屏幕显示并同时调整得分的反应。而玩家们只需了解游戏外在的表现即可,无需弄清背后的工作原理。把内部活动的信息隐藏起来,这就是面向对象的精髓。

由于所有的交流都是通过讯息来实现的,因此只要对象收到的讯息和给出的反应能够适用于新的环境,那么对象就可以从一个上下文情境中提取出来,放入另一个上下文情境。这表示软件对象可以迎合设计师尚未设想到的用途,就像一个设计巧妙的车载收音机既可以装入已有的汽车,也可以装入那些尚未设计出来的汽车。

这样做有什么好处呢?原则上,它能让软件设计人员像土木工程师设计建筑那样构造程序:从工厂购买通用的元件,然后用定制化的软件当做"铆钉"将它们固定到一起。比如说,软件生产商可能会为图片对象定义行为,允许它们旋转、放大、投射、修改颜色、延展等。只要程序中需要操作图片,程序员就可以直接从生产商那里订购图片的"类",然后根据自己的需要在其中添加行为。除此之外,就像预制活动房屋由水槽、马桶、书桌等元件构成一样,程序中新的行为也能由面向对象系统中的元件构建。因此,一个既提供图片又提供搜索功能的图片数据库,其实就是数据库"类"和图片"类"结合的结果。正是因为凯的发明,我们的世界不必再为每一个应用都设计一门新的语言,因为通过创建对象,我们能够模拟任何存在物。

Smalltalk:孩子就是设计师

1972年9月,凯完成了Smalltalk的第一版设计。几天之内,团队成员丹·英格斯(Dan Ingalls)就让语言成功运行。下一个问题是让孩子们学会并使用Smalltalk。凯对此雄心勃勃,他希望Smalltalk能够引发儿童教育方式的改革。在皮亚杰、杰罗姆·布鲁纳 以及鲁道夫·安海姆 的影响下,凯认为图形比单纯的文本更利于儿童的学习。很快地,泰德·克勒(Ted Kaehler)和黛安娜·梅莉(Diana Merry)用Smalltalk编写了基本的制图法,鲍勃·舒尔(Bob Shur)则搭建了动画系统(每秒2到3帧)。

凯招募了阿黛尔·戈德堡 ,还有斯坦福大学的儿童工作者史蒂夫·文依(Steve Weyer)。

1974年戈德堡设计出一种方法,用一个叫做"乔"(Joe)的小图形框来教Smalltalk。"Joe turn 30!"这个命令可以让"乔"旋转30度,"Joe grow 15!"命令可以让"乔"放大15%。图形框还可以被复制(例如把"乔"复制给"吉尔")、连续翻转("forever do"命令)等。

有些孩子学得很快。一个12岁的孩子设计了一个画图系统,和苹果的MacDraw(或者Windows的Draw程序)非常相似。还有个15岁的孩子捣鼓出了一个设计电路的程序。不过据凯所说,在戈德堡教的孩子里只有5%做到了这种超级成就。其他的孩子都很喜欢和"乔"一起工作,但上不了"档次"。无论如何,任何人都能很快上手,这一事实已经说明了设计的质量。

不过,施乐的管理层并不像孩子们这么热情。1976年,官僚主义的惰性占了上风,高层否决了对PARC在硬件和软件设计方面继续供给资源。作为反驳,凯和他的同事们争辩说公司应该敢于冒一点险。在一份备忘录中,凯(正确地)预测在20世纪90年代会出现数以百万计的个人计算机,而且会联结至全球化的公用信息设施(也就是互联网)。他指出,施乐拥有生产力资源、营销基础,麾下还有"绝大多数世界上最优秀的软件设计师",足以称霸市场。遗憾的是这一点未能实现。

他们对这些成果的重大意义完全没有概念--因为没有先例可循。我们也没有为他们做好热身。

就算我们做好了热身,生意人也毕竟不是科学家,他们更像是工程师。一般而言,他们要的不是理解事物,而是想应用。在某些情况下,面对新现象时我们必须花时间去理解它,而不仅只是考虑把它拿来干什么。施乐本身也遇到过这种情况。20世纪50年代时,理特(咨询公司) 也曾预测说静电复印术不可能有市场,但是施乐坚持了下来。

施乐从未下力气追捧过PARC的技术,而苹果公司却正好相反。1979年,史蒂夫·乔布斯、杰夫·拉斯金 和苹果的其他技术专家拜访了凯的团队,参观一个演示程序。他们对所见印象深刻,但最刻骨铭心的莫过于当乔布斯抱怨屏幕显示算法时,丹·英格斯在一分钟之内就解决了问题。乔布斯试图找施乐买下这款软件,但是尽管在苹果小有投资,施乐仍然拒绝了这一建议。

之后的Smalltalk由施乐的一家分支子公司ParcPlace继续运营,依然因为其惊人的可塑性而赢得大批追随者。20世纪90年代,德州仪器(TI)公司有一个编程项目,但对是用当前最流行的C++还是Smalltalk犹豫不决。TI的Smalltalk团队提出进行一次"编码大赛"。他们打算出三个人,与任意数量的C++程序员展开比赛:早晨由中立方指定一个问题,两队同时开始编程,到中午由中立方再对之前的问题进行小范围的改动。比赛的目的在于判断哪一个团队的程序功能更强。在德州仪器的这次试验中,Smalltalk团队轻而易举地取得了胜利。

让校园天翻地覆

1984年凯加盟苹果之前,曾以驻场思想家的身份为雅达利公司 工作了一年。在雅达利,他监督了一些项目,其中的Vivarium是在他到雅达利后才开始构想的,后来成了麻省理工媒体实验室和个性化中心学校(洛杉矶的一所公立学校)的合作项目。

Vivarium让8岁左右的小学生用计算机记录真实数据,并与其他学校的孩子们比较结果。比如说,个性化中心的学生曾记录过学校街区的汽车交通数据,然后与全球范围内其他有相似条件的学校进行比较。通过媒体实验室提供的软件,孩子们可以自行模拟生态系统,并测试诸如"极度饥饿的动物是否会试图吃掉原本捕食自己的动物"这样的问题。

表面上看,我们可以说,凯所构想的个人计算机原型Dynabook已经实现。毕竟笔记本电脑、调制解调器、手写识别系统、数据库存取和超文本链接在今天已经随处可见。但是凯仍然抱有隐忧,他认为计算机有可能会沦为"某种大众鸦片"。

人们想要的东西往往并不是他们真正需要的,因为两者是受不同的原因驱动的。技术应该让愿望和需求保持一致,并同时满足两者。这非常重要。

如果生产出计算机却没有正确的价值体系--这就像生产出钢琴却没有作曲一样--你仍然徘徊在门外,只会简单地弹一些现成的儿歌。我们应该让孩子们有自己的传播媒介。

苏格拉底曾经抱怨过写作。他认为著述会强迫读者接受意见,而不是参与讨论。而计算机远比书本更加生动。不管是公认的观点还是计算机模拟的结果,每一个孩子都应该有机会亲身进行测试、辨别再得出结论。问题是,我们的社会将会鼓励还是压制这种可能性呢?

4.2 / 5 ( 10 votes )

微软产品是怎样炼成的 – 《过河卒》节选 – 戴习为

2021年4月1日 20:38

微软产品是怎样炼成的

——《过河卒》节选
文 / 戴习为

产品,最初来自一个大家认同的好主意

就 D 在瑞特蒙得(Raymond)园区数年来的经验而言,对一位高级软件设计师来说,任何一个研发新产品的设想与建议都是受公司鼓励的,哪怕一时还没有用。如果你又能纠集几位与你差不多的同事认同你的设想,同时也能在公司整个市场战略中找到你设想产品的位置的话,你拿到预算经费的可能性已经很大了。
一个好主意当然也要来自群众。因此,在早期的微软,每隔一段时间,便会将小组内全体人马集中到“与世隔绝”的风景名胜之地。好吃,好喝,好玩,然后便“胡思乱想,畅所欲言”。每个人都要求到台上来畅想一番,你心中的产品应该是个何等模样?其目的无非是一个:集思广益。好主意,当然是“宁肯错抓一千,不可放过一个”,有过此种经验的微软人都会对这种极认真的“玩”记忆犹新。

一个好的产品,起始于一个好的 Spec

光有一个好主意不足以使你心目中的产品形象化,更不能组织生产。虽然有几个同事认同了你的主意,但你还必须把你的产品是什么东西,向方方面面交待清楚。也就是说,你要开始做你的 Spec。
Spec是英文Specification(软件详细加工说明书)的缩写字头,行内人已习惯只说这前四个字母。反正,越简单越好。
一个完善的Spec是美国任何一个大型软件项目最为重要的一步。这在美国软件业同行中恐怕也是全民共识。就如同你要造一辆汽车,总要先把一张张蓝图一丝不苟地画清楚。
不过,也有相当一部分软件工程师在动手做自己想做的软件项目时,并不真的从写 Spec开始。比如 D 吧,直到加入微软前,都没写过Spec。那时候,D 所编写的大多是功能单一的程序。一般是搞明白了每一个程序的输入输出要求,便动手干起来再说。边写边改,十分灵活。一个程序跑通了,结果出来了,也就大功告成了。今天,就算在美国大学里或某些科研机构中,许多人也还是这样做。但程序一大,或者作为一个工业产品,这种干起来再说的办法显然不行了。
实际上,做软件又不同于造汽车。汽车必须一辆一辆地造。软件却只要做一套,如果想批量生产,压拷贝就可以了。有过十几年的软件生涯,在微软又浸泡过几年之后,D 倒是觉得,做一项软件工程可能更像拍一部电影。而Spec则更像是拍电影用的剧本,一剧之本。

到东京去出差,D 曾有机会读过日本人为某个软件项目写的Spec。在这一套Spec中,对该软件产品应该包含的一切,事无巨细,一一定义罗列。计划中的每一个文件与函数都用自己定义的伪语言(一种接近自然语言的算法语言)写出来了。翻着翻着,D 便想,虽然按这本Spec来写程序的程序员,理论上只需将函数中一行行伪码,用写程序时的真实电脑语言置换一下就可以了。但是,其一,程序员一定少不了那种被牵着牛鼻子走的无可奈何的感觉;其二,编写这样一本详细的Spec,几乎相当于用伪码将这个软件写了一遍。这样做,不是将直接写软件会遇到的问题转移到写Spec中来了吗?效率高吗?这个Spec肯定要用极大的努力去写。日本人的确是在拿做汽车的精神在做软件。日本人有他们的道理与风格。

但在D 看来,一个好的电影剧本,有一个故事,有一系列人物和一系列故事发生与展开的时间地点等场景。但在电影的拍摄过程中,从一部电影文学剧本到一部电影,又给导演、演员、摄影、音乐服装等留下了许多发挥的余地与创造的空间。
就像一个好的剧本一样,软件的 Spec 也是从一系列Features(功能,特色)开始的。Feature 这个字应该是指,软件包括哪些功能与特征、软件将要做哪些事情、事情与事情之间的关系、事情与软件用户间的关系。功能多少有点像剧本中的故事与人物。
有了功能的准确定义与描述,接着要设计用户如何使用这些功能。即,你的软件会涉及哪些输入输出设备,比如鼠标、键盘、电脑屏幕与打印机。这就又涉及到,软件产品的各种功能在这些设备上的所有可能的表现形式,在软件业内习惯称为界面①。也就是说,软件用户为了使用这些功能,与这些功能交流对话时用的界面。界面的设计,关系到软件产品是否方便使用,是否通情达理,善解人意。界面反映了你的软件是否具备某种人性。打开PC 的电源,你在电脑屏幕上看到的一个又一个画面,就是 Windows最开始的界面。从一个电影剧本的角度看,界面多少有点类似故事与人物活动时的场景。
过去的 20 年,年复一年,电脑的界面发生着巨大的变化!在20世纪70年代初,电脑的输入手段只限于拨盘开关、穿孔卡片与纸带,最豪华的装备也不过是台电传打字机罢了。输出设备只有一台宽行打印机或者绘图仪。电脑的CRT终端还只能算大型机上尚未普及的奢侈品。因而,“界面设计”从来都没有成为一个软件设计中的主要问题。那时开发人员主要的注意力都在算法上,他们要考虑如何用小得可怜的机器内存与慢得让人焦急的 CPU 来完成计划中的工作。
80年代,图形屏幕终端开始普及,界面设计提上了日程。一门新的学科,软件心理学也随之诞生。今天,在已走进千家万户,甚至人们衣服口袋中的各种电脑中,商业软件的界面已变成了软件设计中举足轻重的一个部分。
功能与界面设计这两个过程,并不一定需要电脑编程人员介入,就如同写剧本并不一定要有导演和演员参与一样。
功能与界面设计,甚至无须电脑专业的人员介入,但设计人员要对该软件所应用的领域有充分的了解,对该领域如何使用电脑要有充分的想象力与创意。设计人员还须对市场上已有相关产品胸中有数,并找准自己的设计在市场上的位置,扬长避短。这其中也应包括了对正在设计的软件与其他市场上同类软件是否“兼容”的一系列技术上与商业上的考虑。
公司大到如微软,设计人员还需要具备一定的人际交流与协调能力,使自己的设计得到市场上大部分同业厂商中潜在的同盟军与合作者认同的能力。

功能与界面的设计完成之后,才是 Coding设计开始之时

完成了上述过程,接下来的设计工作要交给软件工程师来做了。这有点像电影开拍前还需要准确的分镜头剧本一样。工程师们现在要设计和制定一个技术规则了。这个技术规则包括,采用哪些软件技术,用什么样的软件工具语言,在一个什么样的环境与平台上,如何将整个项目分割为一块块的子任务,详细的子任务清单等等细节。这个过程当然也要强调想象力与创造性,但更重要的是技术上的可行性,子任务间相互配合与协调的高效率,以及完成整个功能与界面设计的准确与无误。
一方面,照这样做出来的Spec,对每一个子任务如何完成并未给出更详细的约束。另一方面,所有的界面,与产品应该包含的功能,都通过演示程序在电脑上实时演示过了。
设计中的问题,也通过这些演示看得清清楚楚了。在你的产品设计真正定稿之前,以上过程会反复几次。这是公司里产品研制过程中最民主的阶段。全体人员“没大没小”,“横挑鼻子竖挑眼”,畅所欲言,直到大部分同仁认可为止。
以上步骤,是D 在微软公司工作时,完成一个软件工程Spec 的过程(如果更详细的话,就要变成专题论文了)。到此为止,产品已经是看得见摸得着的东西了。这样,一个Spec可以成文,人手一份了。
有了一个Spec,目标已经很明确了。接下来就是一张抵达目标的时间表,从整个大组,直到每个工程师都要遵循。
这张表,大家习惯称为“Milestone”,也就是公路边那一块块标志着离下一站还有多少公里数的小石碑。这张表,以及将其分解到的每一个小组与个人,(针对个人,又可称做每人的日程表(Schedule),虽然这主要是大小头目的任务,但如果工程师自己有兴趣的话仍有足够的权力参与。值得指出的是,如果已经有了一个全组反复参与下做出的Spec,那么这个时间表的产生,也就很清晰明了,并无太大困难的工作了。但时间表一经确立,就变成约束你自己的纪律了。再说一遍,这是微软公司内最重要的纪律!
目标有了,路线有了,走完每一站的时间也有了,就该轮到一个个自命不凡的研发工程师们上阵了。在一个个的Milestone之间,微软风格的Spec,是留下了足够的余地给他们在编程过程中去“八仙过海,各显神通”。

有了网络,研发管理变得十分“松弛”

瑞特蒙得园区内,研发队伍的管理其实是非常松弛的。
除了极少数临时工(Contractor),每位员工单独拥有自己一间十余平米的办公室,均有一套价值千元、高低任意调节、按人体功能设计的工作台与工作椅。书架、文件柜、白板、至少两套PC,这些都是标准配置。许多人喜欢在自己的房间里贴满各自心爱的照片、卡通,摆放或悬挂花草、盆栽、风铃⋯⋯。将沙发床、健身器、音响设备搬进房间的也不在少数。
看到别人有趣,D 也将 70年代戴着领章帽徽的解放军军人半身标准照放得老大,端挂在墙上。照片之下再标上一行大字:中国军人占领了美国(Chinese Soldier Takes Over USA)!除不考勤上下班时间之外,公司还从早 10 点到下午 2点,每一小时一趟的班车,在园区内与园区边设备豪华的健身俱乐部间穿梭。公司支付一切费用,员工可游泳,或网球⋯⋯,活动筋骨,锻炼身体;一年四季,或小组,或大组,或整个公司,三日一小宴,五日一大宴。美食美酒,Party不断;新上档的电影、热门棒球赛、NBA 篮球赛、橄榄球大赛、歌星演唱会⋯⋯,公司赠票给员工全家,请你务必赏光。一年三百六十五天,公司开满了各式各样的进修课程,鼓励诸位踊跃参加。如诸位能大体上不影响日常工作,而学校又肯收你,读博士、读硕士,公司均乐于为你支付学费。听起来真是神仙过的日子。
其实也不只是“听起来”。头一回参加全公司每年一次的 Party,D 还真的小有震撼。在西雅图湖东区的一个大农场,幸亏有这么大一块地方容得下这么多人和车。那时公司还只有 7000 人,但 7000 辆车密密麻麻地排在一起就已经是很大一片了。那一天,几英里之外,便站满了值勤的警察。
一辆辆贴着微软特别通行证的车辆,打扮如嘉年华会。方圆数百英亩的会场上早已搭起了一座座各色帐篷,摆满中国的春卷、日本的生鱼片、比利时的巧克力、美国的烤牛排、老俄的鱼子酱⋯⋯,几乎全球的各色食品应有尽有。所有人,一律免费自助。不怕你肚子大,就怕你吃不下。所有饮料,包括低度数酒,当然也是无限制供应。各种各样的音乐,混杂着烧烤的焦香在农场上空飘荡。加拿大国家马术队在做着精彩的跳跃。一架架飞机拖着彩带在天上盘旋,一朵朵花伞从天而降直落靶心。这是前来助兴的奥林匹克跳伞队。今天还有吗?五万人了,一天又要吃掉多少吨美食呢?D 还真有点怀念这让微软人自豪的华宴。
不过,公司终究不是疗养院。公司更不可能养“大爷”。任务已经明确了。Spec 已交到你手中,Milestone已清清楚楚。好酒好饭无非是让你上阵时精力充沛、生龙活虎罢了。就如同那第一流的奶牛场,请你听音乐,给你做按摩,为的是请你多出牛奶。
既然公司已待诸位如上宾,吃饱喝足之后,就轮到把诸位的“牵出来溜一溜”了。公司也必须对诸位一举一动了如指掌,方不为过。因此,每天,诸位从使用 Card Key(身份证兼开门的电子钥匙)进入办公大楼开始,你打过的每一个电话,你每一次访问源码库和数据库(作为可选项,你在办公室内电脑上每敲下的一个键),其时间、地点都记录在案。如有必要,均可查询。
一般而言,在你决定要结束一天工作离开办公室之前,应给小组的每一位同事发一个日报告(Daily Report),有事则长,无事则短。这样,同事们相互间知道你今天有哪些值得骄傲的进展,发现了什么问题,有什么需要求助?再加上周报告与月报告,这是不可偷懒的“琐事”。各级头目基本上靠这些电子邮件来了解和掌握工程的进展。(聪明如中国人,便有人也有这个本事,一天便干完预计三天的工作,然后写上三份 Daily Report定时发出,乐得赚来两天假期。多好的管理都有漏洞,但这是以你完成任务为前提的。当头儿的知道不知道也都睁一眼闭一眼了。)
如果有必要,每天三五成群、共进午餐通常是大家一个非正式讨论工作的时候。此外,除非公司有活动,小组通常每周仅有一次例会,探讨那些 E-mail中不容易说清楚和解决的问题。
公司内部不仅是省掉了许多会议,员工所有的公司布告与通知、出差购票与报销、领取杂物、查找资料、借阅图书,微软公司的网络将这些都包办了。行政后勤人员在前方第一线无事可做了。因此,比如D 的部门,除一位为全体人员服务的秘书外,百分之百,包括总经理在内,全部是为第一线干活的工程师。
一张企业网(Intranet),将一个个不考勤、不开会、整年嚼着口香糖的“美国大兵”,紧紧地网在一起,成为一支协调一致的队伍。也使得拼杀在第一线的队伍,将自己的行政与后勤,统统甩给了公司内的少数直属部门。微软创造了在美国也是极高的前后方人数比。网络,用盖茨先生的话讲,是公司的神经系统!

产品的按时完成,源于雷打不动的 Milestone

“Deliver,Deliver And Deliver”,这句话的中文意思是说:一是按时出货,二是按时出货,三还是按时出货。这是在微软瑞特蒙得园区工作的,任何一个研发队伍的大小头目,都要牢记在心的最重要的一句话。相信这也是美国任何一间软件公司都要追求的一个目标。虽然每一个大的软件项目,比如在微软,从 Windows 95 到 Windows XP,几乎每一次都比预计的时间延后投放市场,虽然理想与实际总是有一段距离,但理想总是要追求的。相对而言,对完成Windows这类规模已达几百兆字节、又要涵盖数以万计 PC 硬件厂家的底层系统软件,微软还是做得相当出色的。
员工各自的 Milestone,是从每位工程师到大小头目最重要、也最醒目的目标。花香鸟语的微软园区里,前面讲了那么多事,都可以请随尊便,都是为了在诸位的 Milestone上,容不得半点含糊。Milestone,可以说是微软公司软件产品项目管理中的核心。坦率地讲,90年代初,直到D 离开岗位(以后,就不便妄言了),微软的进度管理是相当苛刻的。新老员工一视同仁,并不会因为你是新加入的员工给你更多的学习时间。
在一群自命不凡的聪明人中,别人可以完成,你却不能,这种压力是可想而知的。在那个年头,无论几点钟走进微软园区,停车场上永远停着一片片的车辆。加班加点是普遍的事情。所以,不考勤的好处就是不必付加班费。将沙发床搬进办公室,连跑回家睡觉的时间都省了。无怪乎,法官大人对垄断案的判词中要指责微软每周平均 60小时的工作时间。
除了电子邮件中乃至会议上就事论事的争论外,美式的日常管理中较少听得到对哪位员工正式的批评意见。会议上听到的大多是“你好我好他也好”的正面鼓励。打工的诸位要想发现与自己相关的问题,就看听话的诸位,能否敏感地直觉到,你好我好中的奥妙与异同。
如果老弟真的糊里糊涂,完不成任务,而又自我感觉良好,那么,一年两次的评审(review )就是毫不含糊地与你清账的时候了:你自己给出一番自我鉴定,你的顶头上司也会给你一个鉴定,一项一项为你打出一个分数来。这次不再会是“你好我好他也好”,这关系到你这半年的奖金、股票、工资乃至你是否还干得下去。因为这也关系到你上司,他的奖金、股票、工资乃至他是否还干得下去。
坦率地讲,无论公司为你准备了多么优秀的工作条件,提供了多好的福利待遇,努力为你营造一个松弛的环境,早期员工所承受的压力也是无须掩饰的事实。压力就是来自Team Job 和雷打不动的 Milestone。
在 D 的微软岁月中,D 未解雇过自己的员工,也未见过开除员工的现象。但用不着多久,完不成任务的员工大多选择自动辞职。以下随便捡出几个实例:
1992 年,D 曾从加州硅谷聘请了一位已有 5年软件工作经验的台湾同胞加盟D 的部门。但上班仅一周,这位同胞便跑来向D 辞职。他抱怨,他无法承受这种不给切入时间便得立即上阵的压力。他没有信心按时完成任务,这与他过去5年的经验不相符合。三十六计,走为上。
1993 年,Visual Basic 未能按时出货,该部门从上到下正职领导全部辞职。
1995 年,一位毕业于中国名校,加入微软前又曾在北京某一知名软件企业工作过数年的北京老乡,跑来向 D 投诉:
他在自己工作部门内受到洋鬼子无法忍受的不公正待遇,希望D 利用自己的位置给予申诉与帮助。D 费尽了力气,才发现,这位自视甚高的老弟实在是忍受不了 Milestone的压力,精神开始变得恍惚了。虽然 D 为此事,求助了许多公司中包括其他中国人在内的同事,也包括今天任微软(中国)公司总经理的唐俊能否为他更换一个压力相对轻微些的位置,均未能解决他的问题。公司毕竟是公司,何况众矢之的微软公司。
1996 年,D 深感自己三板斧已经耍尽,“宝刀”已老,已无法胜任微软公司高级软件设计师的重压,乘着公司尚未讨厌自己之时,向盖茨先生递出辞呈,飘然而去,实也一例。
虽然形式不同,其本质一样。
⋯⋯
对员工的充实与培训是公司的责任。但在美国,照顾弱者通常是社会与各级政府的任务。在战场上,冲不上去的士兵,无论在哪个军队里日子都不好过。

细致的源码管理,与严格的质量控制

Milestone与按时出货,关系着每位员工在公司内的“生死存亡”。但还有更重要的,关系着公司自己的生死存亡,那就是产品的质量!
在美国,不仅是微软,任何一间真正的公司,在他们的“多快好省”总路线中,“好”字永远是摆在首位的。有人说,老美有钱,可以把钱往里砸。但也有人说,这其实更是一种文化。看看上海外滩上那一排排百年沧桑的花岗石大厦,的确是一种文化。再看看北京故宫那金碧辉煌的建筑群,那是更古老的文化。无论各路专家如何诠释这些文化,这里都包含着一件共同的组成部分,让人叹服的质量。
微软研发产品,的确不吝惜金钱。他已花了足够的人力物力去做市场的调查,去琢磨产品的Features(功能、特色),去反复完善产品的设计。在程序员开始动手Coding(编写程序)之后,公司还要组织几乎和研发人员人数 1:1 的测试人员反复测试设计中所有的功能。这是一支千万人的庞大的队伍(如果算上微软外承担了贝塔测试的数千家公司,人数将多达几十万人)。从产品的第一次合成(Build)开始,到阿尔发(α)出货,到β出货,直到投放市场前最后一分钟(已经是数千次合成过了),这支队伍数年如一日,一直都在做着永不停歇的质量测试,不停地寻找一个又一个各式各样的问题。即使是这样,由于一个大型软件的特殊性,它的所有功能间排列组合所产生的近天文数字的不同情况还是难以完全覆盖。这也是为什么每次微软的 Windows产品投放市场之后,仍能发现其中某些问题的原因。庆幸的是,至今为止,尚未发现过严重的弊病。试想,几千万份软件在上亿台PC 上运转,如果也像某些公司的某些做法,将大部分测试交由市场去完成,这样大规模的产品,待到问题“百花齐放”,那垮台的就不仅是微软,一时间,会为全球的 PC 业造成难以估计的灾难。
人们常说,股票价值数千亿美元的微软公司,除了几千个聪明的脑袋就没有什么了(园区虽然漂亮,那几幢楼可值不了这些钱)。脑袋值多少钱,多少有点虚。从这些脑袋里写出的一串串源码才是微软看得见的财产。
微软公司当然有一整套严格的制度与措施。在这套措施的管理之下,你写的每一行码,都会保存得十分妥当和有序。每个程序员的源码都会、也必须定期存档(Check In),这是你创造的财富,也是你的工作纪录。无论什么原因毁坏了你的硬盘,或损毁了某个文件,你自己再也无法挽救时,打个电话,就会有人在一个新硬盘上恢复你昨天下班前所做过的一切。
就D 的部门而言,事前对每位程序员的源码风格不做硬性规定。但每一阶段,小组都会对全组的源码做一次统一的整理(Code Review )。签上各自的姓名,补上必要的说明,并使全组程序简明、易读,书写风格大体一致。一套仔细有序的源码保存与查询体制,不仅仅是质量控制的必要保证(大大加速发现和修改错误的过程),也同时保障了产品的连续性。一个公司才有可能,“走了张屠户,不吃混毛猪”,才能应上那句话,“铁打的营盘,流水的兵”。
源码还必须有一套无懈可击的保护系统。源码在软件公司,犹如钞票在银行。Windows NT 4.0 中文投放市场前夕,北京某个著名电脑公司向微软派来了四位得力干将,作为 OEM 代表,在微软提供的园区内办公室,由微软协助共同解决该公司预装NT 4.0有可能出现的硬件兼容性等技术问题。四位老弟,刚一上班,网上一转,大概是颇有发现,立刻将余事扔到一边,找来软盘,捡其所好,先下载再说。未料到,文件尚未拷完,公司保安人员已站在了办公室门口。
幸亏该北京公司反应及时,立即将四人调回中国,息事宁人,未酿成不必要的风波。
1994年,D 陪同盖茨先生访问北京。访问中,有人当面指出:微软公司的软件价格太高,不符中国国情。盖茨先生回答:既然中国能够接受收彩色电视的价格,为什么不能接受电脑软件的价格呢?
软件靠进口,当然越便宜越好。软件若能出口,那可能就是另一种考虑了。

4.3 / 5 ( 12 votes )
❌
❌