最近,波士顿Northeastern大学的计算机科学家Daniel Kunkle证明了任何一个魔方可以在26步以内解开。这个结果打破了以往所有的记录。在解魔方的处理过程中,他构造了一些非常具有启发性的算法,这篇文章将简单地介绍一下这些算法。
一个魔方大约有4.3 x 10^19种可能的初始状态,再牛的机器也不可能搜索完所有的可能。因此Kunkle和他的指导员Gene Cooperman想出了一些对魔方状态进行分类筛选的策略。
Kunkle和Cooperman首先运用了一个小技巧将问题进行简化。如果魔方的每个面全是一种颜色,我们就认为魔方被解开,不管哪一面是哪一种颜色。换句话说,相互之间可以通过颜色置换得到的初始状态都是等价的。这样,“本质不同”的初始状态就减少到10^18种。
接下来,他们开始观察一类更简单的问题:如果只允许180度转动(half-turn),有多少状态可以被解决。在10^18种状态中,只有大约15000种状态可以仅用180度旋转来破解。对于普通计算机来说,这个数目也不大,只需要不到一天的时间就可以搜索出解开所有15000多个魔方各自需要的最少步数。他们发现,这类初始状态中任何一个都可以在13步以内解决。
然后他们需要做的就是找出,需要多少步才能把任意一种状态转化为这15000种特殊状态中的一个。为了完成这一工作,首先他们把所有的初始状态划分为若干个等价类,每个等价类里的状态都可以仅用180度转动互相得到。这样,同一个等价类中如果任一状态可以变换为其中一种特殊状态,同样的转动步骤也可以使该等价类的其它所有状态都变成特殊状态。最后他们找到了1.4 x 10^12个不同的等价类,需要解决的状态数由最初的4.3x10^19减少到1.4x10^12。但无论如何,10^12仍然是一个恐怖的数字。
现在他们用了一台超级计算机来完成这个工作,并且使用了一些很有技巧性的决策来加速搜索过程。计算机需要耗费大量的时间读取硬盘上的数据,为了加快速度,Kunkle和Cooperman将数据巧妙地进行了处理,使得数据的排列正好与计算机读取的顺序相符,这样就节省了搜索硬盘的时间。
“这种方法可以应用在任何一个组合问题上”,Kunkle说。他提到了西洋跳棋、国际象棋、航班安排和蛋白质摺叠等一系列问题。一种类似的组合学方法最近被用于寻找西洋跳棋的最优策略中。
63小时的计算后,超级计算机得到的答案是,任何一种状态都能在16步以内转化为15000种特殊状态。而这些特殊状态又只需要13步达到最终状态,因此这种方法最终得到的结论是:29步以内可以解决任何一个魔方问题。
但这个数字还不足以创造出新的记录,去年瑞典就曾经得到过27步内解决魔方问题的结论。Kunkle和Cooperman意识到,要想打破这个记录,他们还需要削减3步才行。
应用他们现有的算法,只有8x10^7个状态集合还不能做到26步以内出解。再次对这些相对较少的状态进行搜索,最终他们找到了26步以内解决所有魔方的方法。
7月29日他们在ISSAC(International Symposium on Symbolic and Algebraic Computation,国际符号和代数计算会议)上公布了这一结果。
现在Kunkle和Cooperman希望把最大步骤数减少到25。他们认为他们可以对所有需要26步的状态进行暴力搜索来寻找更优的方案。
虽然他们已经获得了很大的成功,但这一结果很可能还有改进的空间。许多学者认为20步以内足以解决任何魔方,但现在没有人能够证明。
Matrix67翻译,原文地址
做人要厚道,转贴请注明出处
下面分享的是我自己写的三个代码,里面有些题目也是我自己出的。这些代码都是在我的Pascal时代写的,恕不提供C语言了。代码写得并不好,我只是想告诉大家位运算在实战中的应用,包括了搜索和状态压缩DP方面的题目。其实大家可以在网上找到更多用位运算优化的题目,这里整理出一些自己写的代码,只是为了原创系列文章的完整性。这一系列文章到这里就结束了,希望大家能有所收获。
Matrix67原创,转贴请注明出处。
Problem : 费解的开关
题目来源
06年NOIp模拟赛(一) by Matrix67 第四题
问题描述
你玩过“拉灯”游戏吗?25盏灯排成一个5x5的方形。每一个灯都有一个开关,游戏者可以改变它的状态。每一步,游戏者可以改变某一个灯的状态。游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字“1”表示一盏开着的灯,用数字“0”表示关着的灯。下面这种状态
10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:
01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:
01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在6步以内使所有的灯都变亮。
输入格式
第一行有一个正整数n,代表数据中共有n个待解决的游戏初始状态。
以下若干行数据分为n组,每组数据有5行,每行5个字符。每组数据描述了一个游戏的初始状态。各组数据间用一个空行分隔。
对于30%的数据,n<=5;
对于100%的数据,n<=500。
输出格式
输出数据一共有n行,每行有一个小于等于6的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。
对于某一个游戏初始状态,若6步以内无法使所有灯变亮,请输出“-1”。
样例输入
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111
样例输出
3
2
-1
程序代码const
BigPrime=3214567;
MaxStep=6;
type
pointer=^rec;
rec=record
v:longint;
step:integer;
next:pointer;
end;
var
total:longint;
hash:array[0..BigPrime-1]of pointer;
q:array[1..400000]of rec;
function update(a:longint;p:integer):longint;
begin
a:=a xor (1 shl p);
if p mod 5<>0 then a:=a xor (1 shl (p-1));
if (p+1) mod 5<>0 then a:=a xor (1 shl (p+1));
if p<20 then a:=a xor (1 shl (p+5));
if p>4 then a:=a xor (1 shl (p-5));
exit(a);
end;
function find(a:longint;step:integer):boolean;
var
now:pointer;
begin
now:=hash[a mod BigPrime];
while now<>nil do
begin
if now^.v=a then exit(true);
now:=now^.next;
end;
new(now);
now^.v:=a;
now^.step:=step;
now^.next:=hash[a mod BigPrime];
hash[a mod BigPrime]:=now;
total:=total+1;
exit(false);
end;
procedure solve;
var
p:integer;
close:longint=0;
open:longint=1;
begin
find(1 shl 25-1,0);
q[1].v:=1 shl 25-1;
q[1].step:=0;
repeat
inc(close);
for p:=0 to 24 do
if not find(update(q[close].v,p),q[close].step+1) and (q[close].step+1<MaxStep) then
begin
open:=open+1;
q[open].v:=update(q[close].v,p);
q[open].step:=q[close].step+1;
end;
until close>=open;
end;
procedure print(a:longint);
var
now:pointer;
begin
now:=hash[a mod BigPrime];
while now<>nil do
begin
if now^.v=a then
begin
writeln(now^.step);
exit;
end;
now:=now^.next;
end;
writeln(-1);
end;
procedure main;
var
ch:char;
i,j,n:integer;
t:longint;
begin
readln(n);
for i:=1 to n do
begin
t:=0;
for j:=1 to 25 do
begin
read(ch);
t:=t*2+ord(ch)-48;
if j mod 5=0 then readln;
end;
print(t);
if i<n then readln;
end;
end;
begin
solve;
main;
end.
======================= 性感的分割线 =======================
Problem : garden / 和MM逛花园
题目来源
07年Matrix67生日邀请赛第四题
问题描述
花园设计强调,简单就是美。Matrix67常去的花园有着非常简单的布局:花园的所有景点的位置都是“对齐”了的,这些景点可以看作是平面坐标上的格点。相邻的景点之间有小路相连,这些小路全部平行于坐标轴。景点和小路组成了一个“不完整的网格”。
一个典型的花园布局如左图所示。花园布局在6行4列的网格上,花园的16个景点的位置用红色标注在了图中。黑色线条表示景点间的小路,其余灰色部分实际并不存在。
Matrix67 的生日那天,他要带着他的MM在花园里游玩。Matrix67不会带MM两次经过同一个景点,因此每个景点最多被游览一次。他和他的MM边走边聊,他们是如此的投入以致于他们从不会“主动地拐弯”。也就是说,除非前方已没有景点或是前方的景点已经访问过,否则他们会一直往前走下去。当前方景点不存在或已游览过时,Matrix67会带MM另选一个方向继续前进。由于景点个数有限,访问过的景点将越来越多,迟早会出现不能再走的情况(即四个方向上的相邻景点都访问过了),此时他们将结束花园的游览。Matrix67希望知道以这种方式游览花园是否有可能遍历所有的景点。Matrix67可以选择从任意一个景点开始游览,以任意一个景点结束。
在上图所示的花园布局中,一种可能的游览方式如右图所示。这种浏览方式从(1,2)出发,以(2,4)结束,经过每个景点恰好一次。
输入格式
第一行输入两个用空格隔开的正整数m和n,表示花园被布局在m行n列的网格上。
以下m行每行n个字符,字符“0”表示该位置没有景点,字符“1”表示对应位置有景点。这些数字之间没有空格。
输出格式
你的程序需要寻找满足“不主动拐弯”性质且遍历所有景点的游览路线。
如果没有这样的游览路线,请输出一行“Impossible”(不带引号,注意大小写)。
如果存在游览路线,请依次输出你的方案中访问的景点的坐标,每行输出一个。坐标的表示格式为“(x,y)”,代表第x行第y列。
如果有多种方案,你只需要输出其中一种即可。评测系统可以判断你的方案的正确性。
样例输入
6 4
1100
1001
1111
1100
1110
1110
样例输出
(1,2)
(1,1)
(2,1)
(3,1)
(4,1)
(5,1)
(6,1)
(6,2)
(6,3)
(5,3)
(5,2)
(4,2)
(3,2)
(3,3)
(3,4)
(2,4)
数据规模
对于30%的数据,n,m<=5;
对于100%的数据,n,m<=10。
程序代码:program garden;
const
dir:array[1..4,1..2]of integer=
((1,0),(0,1),(-1,0),(0,-1));
type
arr=array[1..10]of integer;
rec=record x,y:integer;end;
var
map:array[0..11,0..11]of boolean;
ans:array[1..100]of rec;
n,m,max:integer;
step:integer=1;
state:arr;
procedure readp;
var
i,j:integer;
ch:char;
begin
readln(m,n);
for i:=1 to n do
begin
for j:=1 to m do
begin
read(ch);
map[i,j]:=(ch='1');
inc(max,ord( map[i,j] ))
end;
readln;
end;
end;
procedure writep;
var
i:integer;
begin
for i:=1 to step do
writeln( '(' , ans[i].x , ',' , ans[i].y , ')' );
end;
procedure solve(x,y:integer);
var
tx,ty,d:integer;
step_cache:integer;
state_cache:arr;
begin
step_cache:=step;
state_cache:=state;
if step=max then
begin
writep;
exit;
end;
for d:=1 to 4 do
begin
tx:=x+dir[d,1];
ty:=y+dir[d,2];
while map[tx,ty] and ( not state[tx] and(1 shl (ty-1) )>0) do
begin
inc(step);
ans[step].x:=tx;
ans[step].y:=ty;
state[tx]:=state[tx] or ( 1 shl (ty-1) );
tx:=tx+dir[d,1];
ty:=ty+dir[d,2];
end;
tx:=tx-dir[d,1];
ty:=ty-dir[d,2];
if (tx<>x) or (ty<>y) then solve(tx,ty);
state:=state_cache;
step:=step_cache;
end;
end;
{====main====}
var
i,j:integer;
begin
assign(input,'garden.in');
reset(input);
assign(output,'garden.out');
rewrite(output);
readp;
for i:=1 to n do
for j:=1 to m do
if map[i,j] then
begin
ans[1].x:=i;
ans[1].y:=j;
state[i]:=1 shl (j-1);
solve(i,j);
state[i]:=0;
end;
close(input);
close(output);
end.
======================= 性感的分割线 =======================
Problem : cowfood / 玉米地
题目来源
USACO月赛
问题描述
农夫约翰购买了一处肥沃的矩形牧场,分成M*N(1<=M<=12; 1<=N<=12)个格子。他想在那里的一些格子中种植美味的玉米。遗憾的是,有些格子区域的土地是贫瘠的,不能耕种。
精明的约翰知道奶牛们进食时不喜欢和别的牛相邻,所以一旦在一个格子中种植玉米,那么他就不会在相邻的格子中种植,即没有两个被选中的格子拥有公共边。他还没有最终确定哪些格子要选择种植玉米。
作为一个思想开明的人,农夫约翰希望考虑所有可行的选择格子种植方案。由于太开明,他还考虑一个格子都不选择的种植方案!请帮助农夫约翰确定种植方案总数。
输入格式:
第一行:两个用空格分隔的整数M和N
第二行到第M+1行:第i+1行描述牧场第i行每个格子的情况,N个用空格分隔的整数,表示这个格子是否可以种植(1表示肥沃的、适合种植,0表示贫瘠的、不可种植)
输出格式
一个整数,农夫约翰可选择的方案总数除以 100,000,000 的余数
样例输入
2 3
1 1 1
0 1 0
样例输出
9
样例说明
给可以种植玉米的格子编号:
1 2 3
4
只种一个格子的方案有四种(1,2,3或4),种植两个格子的方案有三种(13,14或34),种植三个格子的方案有一种(134),还有一种什么格子都不种。
4+3+1+1=9。
数据规模
对于30%的数据,N,M<=4;
对于100%的数据,N,M<=12。
程序代码:program cowfood;
const
d=100000000;
MaxN=12;
var
f:array[0..MaxN,1..2000]of longint;
w:array[1..2000,1..2000]of boolean;
st:array[0..2000]of integer;
map:array[0..MaxN]of integer;
m,n:integer;
function Impossible(a:integer):boolean;
var
i:integer;
flag:boolean=false;
begin
for i:=1 to MaxN do
begin
if flag and (a and 1=1) then exit(true);
flag:=(a and 1=1);
a:=a shr 1;
end;
exit(false);
end;
function Conflict(a,b:integer):boolean;
var
i:integer;
begin
for i:=1 to MaxN do
begin
if (a and 1=1) and (b and 1=1) then exit(true);
a:=a shr 1;
b:=b shr 1;
end;
exit(false);
end;
function CanPlace(a,b:integer):boolean;
begin
exit(a or b=b);
end;
procedure FindSt;
var
i:integer;
begin
for i:=0 to 1 shl MaxN-1 do
if not Impossible(i) then
begin
inc(st[0]);
st[st[0]]:=i;
end;
end;
procedure Init;
var
i,j:integer;
begin
for i:=1 to st[0] do
for j:=i to st[0] do
if not Conflict(st[i],st[j]) then
begin
w[i,j]:=true;
w[j,i]:=true;
end;
end;
procedure Readp;
var
i,j,t,v:integer;
begin
readln(m,n);
for i:=1 to m do
begin
v:=0;
for j:=1 to n do
begin
read(t);
v:=v*2+t;
end;
map[i]:=v;
readln;
end;
end;
procedure Solve;
var
i,j,k:integer;
begin
f[0,1]:=1;
map[0]:=1 shl n-1;
for i:=1 to m do
for j:=1 to st[0] do
if not CanPlace(st[j],map[i]) then f[i,j]:=-1 else
for k:=1 to st[0] do if (f[i-1,k]<>-1) and w[j,k] then
f[i,j]:=(f[i,j]+f[i-1,k]) mod d;
end;
procedure Writep;
var
j:integer;
ans:longint=0;
begin
for j:=1 to st[0] do
if f[m,j]<>-1 then ans:=(ans+f[m,j]) mod d;
writeln(ans);
end;
begin
assign(input,'cowfood.in');
reset(input);
assign(output,'cowfood.out');
rewrite(output);
FindSt;
Init;
Readp;
Solve;
Writep;
close(input);
close(output);
end.
今天我们来看两个稍微复杂一点的例子。
n皇后问题位运算版
n皇后问题是啥我就不说了吧,学编程的肯定都见过。下面的十多行代码是n皇后问题的一个高效位运算程序,看到过的人都夸它牛。初始时,upperlim:=(1 shl n)-1。主程序调用test(0,0,0)后sum的值就是n皇后总的解数。拿这个去交USACO,0.3s,暴爽。procedure test(row,ld,rd:longint);
var
pos,p:longint;
begin
{ 1} if row<>upperlim then
{ 2} begin
{ 3} pos:=upperlim and not (row or ld or rd);
{ 4} while pos<>0 do
{ 5} begin
{ 6} p:=pos and -pos;
{ 7} pos:=pos-p;
{ 8} test(row+p,(ld+p)shl 1,(rd+p)shr 1);
{ 9} end;
{10} end
{11} else inc(sum);
end;
乍一看似乎完全摸不着头脑,实际上整个程序是非常容易理解的。这里还是建议大家自己单步运行一探究竟,实在没研究出来再看下面的解说。

和普通算法一样,这是一个递归过程,程序一行一行地寻找可以放皇后的地方。过程带三个参数,row、ld和rd,分别表示在纵列和两个对角线方向的限制条件下这一行的哪些地方不能放。我们以6x6的棋盘为例,看看程序是怎么工作的。假设现在已经递归到第四层,前三层放的子已经标在左图上了。红色、蓝色和绿色的线分别表示三个方向上有冲突的位置,位于该行上的冲突位置就用row、ld和rd中的1来表示。把它们三个并起来,得到该行所有的禁位,取反后就得到所有可以放的位置(用pos来表示)。前面说过-a相当于not a + 1,这里的代码第6行就相当于pos and (not pos + 1),其结果是取出最右边的那个1。这样,p就表示该行的某个可以放子的位置,把它从pos中移除并递归调用test过程。注意递归调用时三个参数的变化,每个参数都加上了一个禁位,但两个对角线方向的禁位对下一行的影响需要平移一位。最后,如果递归到某个时候发现row=111111了,说明六个皇后全放进去了,此时程序从第1行跳到第11行,找到的解的个数加一。
~~~~====~~~~===== 华丽的分割线 =====~~~~====~~~~
Gray码
假如我有4个潜在的GF,我需要决定最终到底和谁在一起。一个简单的办法就是,依次和每个MM交往一段时间,最后选择给我带来的“满意度”最大的MM。但看了dd牛的理论后,事情开始变得复杂了:我可以选择和多个MM在一起。这样,需要考核的状态变成了2^4=16种(当然包括0000这一状态,因为我有可能是玻璃)。现在的问题就是,我应该用什么顺序来遍历这16种状态呢?
传统的做法是,用二进制数的顺序来遍历所有可能的组合。也就是说,我需要以0000->0001->0010->0011->0100->...->1111这样的顺序对每种状态进行测试。这个顺序很不科学,很多时候状态的转移都很耗时。比如从0111到1000时我需要暂时甩掉当前所有的3个MM,然后去把第4个MM。同时改变所有MM与我的关系是一件何等巨大的工程啊。因此,我希望知道,是否有一种方法可以使得,从没有MM这一状态出发,每次只改变我和一个MM的关系(追或者甩),15次操作后恰好遍历完所有可能的组合(最终状态不一定是1111)。大家自己先试一试看行不行。
解决这个问题的方法很巧妙。我们来说明,假如我们已经知道了n=2时的合法遍历顺序,我们如何得到n=3的遍历顺序。显然,n=2的遍历顺序如下:
00
01
11
10
你可能已经想到了如何把上面的遍历顺序扩展到n=3的情况。n=3时一共有8种状态,其中前面4个把n=2的遍历顺序照搬下来,然后把它们对称翻折下去并在最前面加上1作为后面4个状态:
000
001
011
010 ↑
--------
110 ↓
111
101
100
用这种方法得到的遍历顺序显然符合要求。首先,上面8个状态恰好是n=3时的所有8种组合,因为它们是在n=2的全部四种组合的基础上考虑选不选第3个元素所得到的。然后我们看到,后面一半的状态应该和前面一半一样满足“相邻状态间仅一位不同”的限制,而“镜面”处则是最前面那一位数不同。再次翻折三阶遍历顺序,我们就得到了刚才的问题的答案:
0000
0001
0011
0010
0110
0111
0101
0100
1100
1101
1111
1110
1010
1011
1001
1000
这种遍历顺序作为一种编码方式存在,叫做Gray码(写个中文让蜘蛛来抓:格雷码)。它的应用范围很广。比如,n阶的Gray码相当于在n维立方体上的Hamilton回路,因为沿着立方体上的边走一步,n维坐标中只会有一个值改变。再比如,Gray码和Hanoi塔问题等价。Gray码改变的是第几个数,Hanoi塔就该移动哪个盘子。比如,3阶的Gray码每次改变的元素所在位置依次为1-2-1-3-1-2-1,这正好是3阶Hanoi塔每次移动盘子编号。如果我们可以快速求出Gray码的第n个数是多少,我们就可以输出任意步数后Hanoi塔的移动步骤。现在我告诉你,Gray码的第n个数(从0算起)是n xor (n shr 1),你能想出来这是为什么吗?先自己想想吧。
下面我们把二进制数和Gray码都写在下面,可以看到左边的数异或自身右移的结果就等于右边的数。
二进制数 Gray码
000 000
001 001
010 011
011 010
100 110
101 111
110 101
111 100
从二进制数的角度看,“镜像”位置上的数即是对原数进行not运算后的结果。比如,第3个数010和倒数第3个数101的每一位都正好相反。假设这两个数分别为x和y,那么x xor (x shr 1)和y xor (y shr 1)的结果只有一点不同:后者的首位是1,前者的首位是0。而这正好是Gray码的生成方法。这就说明了,Gray码的第n个数确实是n xor (n shr 1)。
今年四月份mashuo给我看了这道题,是二维意义上的Gray码。题目大意是说,把0到2^(n+m)-1的数写成2^n * 2^m的矩阵,使得位置相邻两数的二进制表示只有一位之差。答案其实很简单,所有数都是由m位的Gray码和n位Gray码拼接而成,需要用左移操作和or运算完成。完整的代码如下:var
x,y,m,n,u:longint;
begin
readln(m,n);
for x:=0 to 1 shl m-1 do begin
u:=(x xor (x shr 1)) shl n; //输出数的左边是一个m位的Gray码
for y:=0 to 1 shl n-1 do
write(u or (y xor (y shr 1)),' '); //并上一个n位Gray码
writeln;
end;
end.
Matrix67原创
转贴请注明出处
题目在这里:http://www.matrix67.com/blog/article.asp?id=241
如果机房马上要关门了,或者你急着要和MM约会,请看简要题解:
1. 用类似于传统hanoi的递归方法可以做到3^n-1次。这显然是最多的了,因为总的状态数也只有3^n个。
2. 可以证明,竞赛图中不存在环当且仅当所有顶点的出度从小到大排列依次为0, 1, 2, ... , n-1 。
3. 在最短路树上做树状DP,需要多叉转二叉。注意几种需要输出0的情况。
4. 搜索,算是练基本功了。用位运算优化,不加任何剪枝就能过。
否则,请慢慢阅读——
Problem 1: 为什么最少
如果你还不熟悉Hanoi塔的解法,去题目中提到的那篇日志看看吧。如果你已经熟悉Hanoi塔的解法,你会立刻想到这道题的解法:依然是递归地解决。至于怎么递归,样例已经告诉我们了:把前n-1个金片从1号柱搬到3号柱,把第n片移到2号柱,又把那n-1片从3号柱搬回1号柱,再把第n片搬到3号柱,最后把那n-1个金片又搬过来,完成整个操作。
我们下面解决三个问题:为什么这样不会重复出现状态,这样的移动步数是多少,为什么这样的操作步数是最多的。
为什么这样不会出现重复的状态呢?因为我们假设前n-1个金片的移动过程中没有重复状态,而三次对n-1的调用时整个状态由于第n个金片的位置不同而不同。
这样的方法获得的操作步数是多少呢?答案是3^n-1。我们可以用数学归纳法证明,n=1时步数为2显然正确,而f(n+1)=3f(n)+2=3*(3^n-1)+2=3^(n+1)-1。
为什么这样的操作步数是最多的呢?废话,这样的操作步数当然是最多的,因为总的状态数也只有3^n个(每个金片的三种可能的位置确定了一种状态),你的移动步骤能比这个数目还多就见鬼了。
这道题有人用了math库,没有提供math库导致无法编译是我的失误,向大家道歉。
Hanoi问题的变种太多了,比如多根柱子、单向移动、双色金片等等。dd上次不是也出了一题么。
这题代码很短,我公布在下面。program whyleast;
procedure solve(t,a,b:integer);
begin
if t=0 then exit else
begin
solve(t-1,a,b);
writeln(a,' ',2);
solve(t-1,b,a);
writeln(2,' ',b);
solve(t-1,a,b);
end;
end;
{====main====}
var
n,i:integer;
ans:longint=1;
begin
assign(input,'whyleast.in');
reset(input);
assign(output,'whyleast.out');
rewrite(output);
readln(n);
for i:=1 to n do ans:=ans*3;
writeln(ans-1);
solve(n,1,3);
close(input);
close(output);
end.
Problem 2: 身高控制计划
一个竞赛图是指任两个点之间都有一条有向边的图。竞赛图有很多奇妙的性质,比如一个竞赛图必然存在一条经过所有节点的路等等。
下面我们证明,竞赛图中不存在环当且仅当所有顶点的出度从小到大排列依次为0, 1, 2, ... , n-1 :
如果一个有向图的所有点出度都至少为1,那么这个图一定有环,因为在找到环之前DFS总可以找到新的节点。如果有向图无环,必然存在一个点没有出度。由于任两点之间都有有向边,那么其它所有点都要连一条边指向它,这样其它所有点的出度都至少为1了。删掉这个出度为0的点后剩下的图仍然无环,不断对剩下的图继续上面的过程就得到了我们的结论。
现在我们的算法就很明确了,首先统计初始状态下的出度,然后设计某种数据结构完成两种操作:改变一个数(加一减一),询问所有数是否恰好为0, 1, 2, ... , n-1 。
统计初始状态下的出度方法有很多,这里介绍两个。首先对身高排序,然后对于每个人进行二分,寻找有序数列中该数的4/5和5/4各在什么地方。还有一种方法也是先排序,然后从左到右扫描整个序列,并保持两个指针始终指向4/5和5/4处。每次开始处理一个新的数时都把两个指针适当地右移直到超出了这个数的4/5或5/4为止。两种方法都是O(nlogn)。别以为第二种方法是线性的哦,线性算法之前还有一个排序呢。
操作的处理也有不少方法。最简单的方法就是统计当前图中出度的数目有多少种。就是说,用a[i]表示出度为i的点有多少个,然后不断更新a[i]>0的有多少个。当这个数目等于n时我们就认为图中没有环(因为出度可能的取值只有从0到n-1共n种)。
注意,由于同一条边可能被操作多次,因此需要一个Hash表(开散列)来判重。具体地说,你需要判断这条边以前被操作过奇数次还是偶数次,以决定哪边的出度要增加,哪边的出度要减小。
Problem 3: 狼的复仇
把这个问题中所有的最短路都画出来是什么样子?它一定是一棵树!为什么?首先,图肯定是连通的,因为源点到任一个点都有一条最短路;其次,图肯定无环,因为源点到任一个点只有一条最短路(环的出现将意味着某些点有更短的路存在)。仔细想一下Dijkstra的算法过程,不难想到Dijkstra算法的实质就是在建这棵树——每一次由x节点加上边x-y扩展到y节点就记作x是y的父亲。注意观察上图中左图是如何变成右图的。这样,题目变成了这种形式:在有根树上,除根节点之外的其它节点中选择一些节点,使得这些节点和它们所有祖先的权值和最大。这是一个经典的树型动态规划模型。我们用f[i,j]表示以节点i为根节点的子树花费j个单位的材料最多可以得到多大的攻击力。令节点1的材料和攻击力都为0,那么最后输出f[1,0..k]中的最大值即可。决策分为两类,要么该位置建一个塔,要么把所有材料适当地分给儿子(自己就不需要再建了)。但这样的复杂度太高,我们需要用DP嵌套或者更巧妙的多叉转二叉来解决。
DP嵌套理解起来更简单,它主要是解决这样一个子问题:若某个节点有m个儿子,我们需要寻找当j1+j2+...+jm等于某个定值时f[儿子1,j1]+f[儿子2,j2]+...+f[儿子m,jm]的最大值。这个子问题与我的某次模拟赛中论文课题选择那道DP题几乎是一模一样,看一看那道题就明白了。下面简单描述多叉转二叉的方法。
如果你还不知道多叉转二叉的话,这道题是一个绝好的学习材料。一棵多叉树可以用“左儿子右兄弟”的方法转为二叉树,具体的说就是把多叉树转化为这种形式:节点的左儿子才是真正的儿子,节点的右儿子只是和它同辈的兄弟。注意看上图的左图是如何变成右图的。现在,我们的f[i,j]表示在这棵二叉树上的子树花费j个材料的最大值。它有这样五种决策:
1. 自己把材料用了;
2. 自己把材料用了后剩下的给右边;
3. j个材料全部用到左边去,加上自己的权值;
4. j个材料全部用到右边去,不加自己的权值;
5. j个材料左边用一点,右边用一点,加上自己的权值。
注意思考决策3-5中为什么有的决策要算自己的权值,有的不算自己的权值。转化后的二叉树并不是原来的树,左边和右边的意义是不同的。在原图中对比一下你就能看到这些决策的实质了。
状态O(nk)个,决策为O(k),因此这道题可以在O(nk^2)的时间内解决。
这题有很多细节需要注意。比如,建树时如何处理多条最短路优先选择编号较小节点,解决方法是在Dijkstra的更新过程中,当新的最短路与原来相同但新的父亲比原父亲编号小时仍然要更新。还有几种特殊情况需要输出0,比如所有的花费都大于k时,再比如所有的攻击力都小于0时。
Problem 4: 和MM逛花园
这题搜索,DFS枚举方向,没什么好说的。在这里说一下位运算优化。
用State[x]表示第x行的状态(0表示还没走过,1表示已经走过),用Map[x,y]表示地图第x行第y列的位置是否有景点。假如当前位置是(x,y),枚举方向d后进行下面的while循环可以飞快地确定最终状态。细心的人会发现State的储存是“反”的(和实际状态左右颠倒)。这没关系,只要State的储存一直是反的就没事了。const
dir:array[1..4,1..2]of integer=
((1,0),(0,1),(-1,0),(0,-1));while Map[x,y] and ( not State[x] and (1 shl (y-1) )>0) do
begin
x:=x+dir[d,1];
y:=y+dir[d,2];
State[x]:=State[x] or ( 1 shl (y-1) );
...
end;
我的数据都是唯一解,因此你不用麻烦着写check了。
Matrix67原创
转贴请注明出处
大家帮忙校对,我先睡觉了
不少人可能为找不到非传统题型的练习题而头疼。这几天我专门发出一些用于省选集训的题目供大家参考。
Problem 1: cell 手机
题目来源:USACO Contest FEB06 Gold (Translated by Matrix67)
问题描述
奶牛们已经开始使用手机交流了,但它们发现手机的按键设计不适合它们的蹄子。它们想设计一个新的手机,让它的按键更少,但是更大。
它们喜欢普通手机的一个功能:词语联想。每个按键都有一些字母和它对应,打出一个单词只需要按对应的按键。因为一个按键可能对应多个字母,因此某些单词可能会发生“歧意”。不过,大多数时候这种歧意可以通过用字典判断的方法来消除。
考虑到奶牛们在自定义一款新的手机,它们还需要用奶牛字母表替换英文字母表。神奇的是,奶牛字母表中的字母恰好是英语字母表中的前L个字母,即使顺序也一样。它们想知道如何把这些字母分配给B个按键(1<=B<=L)使得在字典中不会产生歧意的单词最多。就像普通的手机一样,他们希望每个按钮上的字母都是字母表中一段连续的字母。
这是一个答案提交类的题目。你只需要在你自己的计算机上计算出你的答案,然后把输出文件提交上来。与输入文件cell.3.in相对应的输出文件应该为cell.3.out,这里“3”表示你提交的答案是第3个输入文件的解。当然,其它输出文件需要把这个3替换成相应的数字。你不需要提交任何其它的文件。
输入数据
第一行:一个整数N,表示这是第N个输入文件。
第二行:两个用空格隔开的整数:B和L
第三行:D,字典中的单词数(1<=D<=1000)
第四行到第D+3行:每一行包含一个字典中的单词,用1到10个大写字母表示。这些单词按照字典序给出,并且保证没有重复。
输出数据
第一行:字典中具有唯一的按钮序列的单词数。
第二行到第B+1行:其中的第n行包含有第n个按钮上的字母,用大写的字母按照字典的顺序表示。所有行必须按照字典序排列,每个字母出现恰好一次。如果有多个最优解,选用第一个按键上字母最多的解。如果最优解仍然不唯一,考虑第二个按键上字母最多,依此类推。
样例输入(cell.1.in)
1
3 13
11
ALL
BALL
BELL
CALK
CALL
CELL
DILL
FILL
FILM
ILL
MILK
样例输出(cell.1.out)
7
AB
CDEFGHIJK
LM
样例说明
第一个按键上只有AB两个字母,第二个按键上含有C到K,第三个按键上为LM。单词CELL、DILL、FILL和FILM的输入都是2233,其它7个单词的输入都是唯一的。
题解(Ctrl+A):
这道题目……搜索,乱搞。
Problem 2: selfstr 自描述序列
题目来源:Matrix67根据经典问题改编
问题描述
“这句话里有1个数字零,2个数字一,1个数字二,0个数字三”。
在N(N>=2)进制中只允许0到N-1这N个数字出现。一个N位的N进制数(允许有前导0)可以由另一个同样多位的数来描述。我们定义一个N位N进制数的描述序列为:左起第i个数字为原数中数字i-1出现的次数。
例如,在4进制中,0023的描述序列为2011,因为0023中有2个0,0个1,1个2和1个3。
我们惊奇地发现,4进制数1210的描述序列是它本身!我们称这样的数叫做“自描述序列”。
你需要编写程序计算出在某个进制下的自描述序列。一个进制下的自描述序列可能有很多个,你只需要给出其中一个即可。
这是一个答案提交类的问题。你只需要在你自己的计算机上计算出你的答案,然后把输出文件提交上来。与输入文件selfstr.3.in相对应的输出文件应该为selfstr.3.out,这里“3”表示你提交的答案是第3个输入文件的解。当然,其它输出文件需要把这个3替换成相应的数字。你不需要提交任何其它的文件。
输入格式
输入数据只有一个正整数N
输出格式
输出N个字符,它表示N进制下的自描述序列。在高于10的进位制下,大于9的数字请用大写字母表示。
如果有多种可能的解,你只需要输出其中一个。
如果该进制下无解,请输出“NONE”。
样例输入(selfstr.1.in)
4
样例输出(selfstr.1.out)
1210
题解:
这道题太有意思了!首先,你需要先算几个小数据。你会发现,算到N>=6后,渐渐有规律了:
N N进制下的自描述序列
4 1210 or 2020
5 21200
6 NONE
7 3211000
8 42101000
9 521001000
事实上,这道题目就是考你当搜索到一些解后能不能找到规律得到所有解。这里我们发现,对所有N>6,至少存在一个解为R21(0...0)1000,其中R=N-4,中间0的个数为N-7。结论显然正确。
有可能除了这个之外存在其它的解,因此我们仍然需要写一个check来核对答案。
Problem 3: relation 大小关系
题目来源:Matrix67根据经典问题改编
问题描述
用关系“ < ”和“ = ”将3个数a、b、c依次序排列时,有13种不同的序列关系:
a=b=c, a=b<c, a<b=c, a<b<c, a<c<b
a=c<b, b<a=c, b<a<c, b<c<a, b=c<a
c<a=b, c<a<b, c<b<a
用这两种关系连接N个数有多少种不同的方案?
这是一个答案提交类的问题。所有选手将得到10个输入数据,你只需要在你自己的计算机上计算出你的答案,然后把你的答案提交上来。与输入文件relation.x.in相对应的输出文件应该为relation.x.out,这里x表示一个1到10之间的数。
输入格式
输入一个整数,表示N。
输出格式
输出用小于和等于符号将N个数进行有序排列的方案数。
样例输入(relation.1.in)
3
样例输出(relation.1.out)
13
题解:
组合数学+高精度。由于数据规模很小,我就直接搞成了答案提交类的题目。
下面给出两种递推方法:
Solution 1: N个数中必然存在一个最大的“等价类”,如果这个等价类里有k个数,那么剩下的数就有F(N-k)种排列方案。别忘了我们需要枚举这k个数是哪k个数。于是,F(N)=C(N,1)F(N-1)+C(N,2)F(N-2)+C(N,3)F(N-3)+ ... +C(N,N)F(0)
Solution 2: 用F[ i, j]表示 i个数中有j 个等价类的排列方案(就是说有j-1个小于符号)。第 i个数有可能并入了F[i-1, j]中的 j个等价类中的一个,也有可能不与任何一个已有的数相等,独自成为一个等价类插入F[i-1, j-1]里产生的 j个空位中。于是,F[ i,j ]=F[i-1, j]*j + F[i-1,j-1]*j。
其它问题:
如何用Cena评测答案提交类问题?
见http://www.matrix67.com/blog/article.asp?id=176
这些题的数据哪里有?
第一题:http://ace.delos.com/FEB06,GOLD DIVISION里面的第三个
第二题:自己写check,不需要数据
第三题:http://www.research.att.com/~njas/sequences/b000670.txt,吓死你
Matrix67原创
转贴请注明出处












