算法合集之《信息学中守恒法的应用》(不错的文章保存一下)

Comments: No Comments
Published on: 2011 年 01 月 19 日

【摘要】本文提出和总结了“守恒法”,以及它在信
息学竞赛中的一些应用。守恒的本质是寻找变化中的
不变量。守恒法能帮助我们跳过、避开纷繁复杂的细
节,直接看透问题的本质。
【关键字】守恒法 不变量
【正文】
一、 引言
现实生活和实际问题是纷繁复杂的。
问题1 两个质量相等的小球,速度分别为5m/s, 4m/s,他们相向运动,完
全弹性碰撞之后速度分别变成多少?
问题2 10g C 和10g O2在密闭容器中反应一个小时。最后的总质量是多少?
问题1 我们大概耳熟能详:动量守恒、动能守恒,两个方程就能解出速度。
实际上小球碰撞的过程是复杂的,究竟两对力如何互相作用、互相影响、如何做
功,思考起来是非常的复杂。如果忽略它们变化的具体过程,我们很容易发现“动
量”和“动能”这两个变化中的不变量,抓住不变量,就能跳过繁琐的细节,直
达目标。
问题2 也是类似的题目。C 和O2的反应同样是复杂的。在不同的局部,条件
不同,可能产生CO,也可能产生CO2;CO2和C 还可能重新转化为CO……事实上
不可能有人列出三个化学方程去分析——在一个密闭容器中,无论怎么变,总质
量必然不变——也就是质量守恒。抓住这一点,我们在1 秒钟内就能说出答案:
20g。
以上两个例子生动的说明守恒的作用。
现实生活和实际问题如此纷繁复杂,条件和变化如此之多,以至于我们考虑
稍不周密就可能全盘皆错;抑或限于问题本身的复杂性,根本无法分析。
但是如果能找到一两个守恒量——也就是变化中的不变量,那么问题就能大
大的简化了。忽略细节,抓住主要矛盾,这就是守恒法。

二、 一个简单的例子
http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=109 这个例子NYOJ有,有兴趣的去做做,看过该文比较easy~
例题1 有一个数列a1, a2, a3, ……, an。每次可以从中任意选3 个相邻的数ai-1,
ai, ai+1,进行如下操作(此操作称为“对ai进行操作”)
(ai-1, ai, ai+1)à(ai-1+ai, -ai, ai+ai+1)

给定初始和目标序列。问:能不能通过以上操作,将初始序列转换到目标序
列。 比如初始(1 6 9 4 2 0),目标(7 -6 19 2 -6 6)。可以经过如下操作:
(1 6 9 4 2 0)à( 1 6 13 -4 6 0)à
(1 6 13 2 -6 6)à( 7 -6 19 2 -6 6)
(加粗的是每次被操作的ai)
如果初始序列是(1 2 3),目标是(1 3 2)。那么无论如何都不能通过操作从
初始序列转换到目标序列。
Input.txt Output.txt
1 6 9 4 2 0 Yes
7 -6 19 2 -6 6
Input.txt Output.txt
1 2 3 No
1 3 2
我们分析一下这个题目。
操作是有先后顺序之分的。比如先对a2操作,再操作a3;先对a3操作,再
操作a2,结果就有天壤之别。
观察Sample:
(1 6 9 4 2 0)à(1 6 13 -4 6 0)à
(1 6 13 2 -6 6)à(7 -6 19 2 -6 6)
数字杂乱无章没有规律。仔细观察一下操作规则:
(ai-1, ai, ai+1)à(ai-1+ai, -ai, ai+ai+1)
直观的看,相当于把中间的数分别加到两边去,然后取反。容易发现,操作
前后的总和是不变的!
我们可能很激动的猜想:只要两个序列和相等,他们就能通过操作互达。
但是第二个sample 很快否定了这个想法:(1 2 3), (1 3 2)是不可达的。
因为(1 2 3)能进行的操作仅仅是:(3 -2 5),再进行一次操作回到(1 2 3)。所
以永远不能变成(1 3 2)。
总和虽然不行,我们可以试着考察局部和。
(ai-1, ai, ai+1) (ai-1+ai, -ai, ai+ai+1)
S1=ai-1 S1’=ai-1+ai
S2=ai-1+ai S2’=ai-1
S3=ai-1+ai+ai+1 S3’=ai-1+ai+ai+1
很容易看出S3=S3’,(S1,S2)=(S2’,S1’)。也就是说把(S1,S2,S3)中的S1和S2交换
位置就能得到(S1’,S2’,S3’)。
稍微推广一下:设Si=a1+a2+…+ai,对(ai-1, ai, ai+1)操作本质上和交换Si-1, Si
是等价的。
比如第一个Sample:
(1 6 9 4 2 0)à(1 6 13 -4 6 0)à
(1 6 13 2 -6 6)à(7 -6 19 2 -6 6)
转化成和之后:
(1 7 16 20 22 22)à(1 7 20 16 22 22)à
(1 7 20 22 16 22)à(7 1 20 22 16 22)

(加粗的是交换的两个数)
比如第二个Sample:
(1 2 3)àS(1 3 6)
(1 3 2)àS(1 4 6)
对(1 2 3)的操作实质上是不断的交换S(1 3 6)中的1, 3。无论如何也不可
能变成(1 4 6),因为4 根本没在S(1 3 6)中出现过!
另外还有一点,参与交换的只有S1~Sn-1,Sn是雷打不动的。所以我们算法出
来了:
1、判断Sn是否相等。
2、判断{S1, S2 ,…, Sn-1}是否相等。
O(nlogn)的时间复杂度(排序复杂度)。
一个看似繁琐的题目被很轻松的解决了。
如上文所言,操作的变化过程是纷繁复杂的,可以说没任何规律。如果从
每一次操作对总体的贡献入手研究,会碰得头破血流。
但是我们把原来的数列进行了一个小小的转化:求和。正是这个求和,使得
操作的本质浮出水面,操作由令人头晕目眩的(ai-1, ai, ai+1)à(ai-1+ai, -ai, ai+ai+1)、
变成了简单的“交换Si-1和Si”。
这是一个应用守恒的经典例子。其中的守恒量是{S1,S2, …,Sn-1},这个集合始
终保持不变,不会有新的元素生成、也不会无缘无故有元素消失;有的只是顺序
上的交换。
抛开琐碎的细节,我们抓住了本质。
为什么会想到求和呢?
因为(ai-1, ai, ai+1)à(ai-1+ai, -ai, ai+ai+1)的结构很容易让人发现它的总和守恒;进
一步联想到局部和。
问题本身的结构是守恒量选择的主要依据。
联想和化归是构造守恒量的关键。
此题特点明显,我们再来看一个难一点的试题。进一步领略守恒法的魅力。

//网上有篇守恒法则的应用……  
#include<iostream>  
#include<algorithm>  
using namespace std;  
int a[1003],b[1003];  
int main()  
{  
    int n,m;  
    cin>>n;  
    while(n--)  
    {  
        cin>>m;  
        for (int i=1;i<=m;i++)  
        {  
            cin>>a[i];  
            a[i] +=a[i-1];  
        }  
        for (int i=1;i<=m;i++)  
        {  
            cin>>b[i];  
            b[i] +=b[i-1];  
        }  
        sort(a+1,a+m);  
        sort(b+1,b+m);  
        if(equal(a+1,a+m,b+1)) cout<<"Yes"<<endl;  
        else cout<<"No"<<endl;  
    }  
    return 0;  
}       

三、 一个隐蔽的例子
这个题为方便大家学习测试,我把这个题目出到了OJ上。
http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=146
例2 跳棋(Jump, POI IV Stage I Problem 2)
有一列无限长的格子里面,某些格子里面放了棋子。如果某个格子里面有棋
子,就可以拿走这一颗,并且在这个格子的左边两个格子里面各放一颗。

如果连续两个格子里面都有棋子,可以分别在两个格子里面各拿走一颗,并
2 6 3
2 1 1 5 3
且在它们右边的格子里面放一颗。
现在的任务是:

给定初始状态,要求使用以上操作,使得:
1、每个格子至多只有1 颗棋子
2、没有相邻的两个格子都有棋子。
简单的说,就是无法继续操作下去了!
下面给出一个例子:
Input.txt Output.txt
0 0 1 0 0 2 0 0 1 0 0 0 0 1 0 1 0 1 0 0 0
下面我们来分析一下这个问题。
这又是一个操作问题。要求不存在格子有>1 的棋子,现在考虑如果一个孤立
的格子里面有3 颗棋子该怎么办:

2 6 3
2 1 1 5 3
1 2 1
1 1 1 1 1
1 1 1 1
1 1 1
3
通过这样的转化就能把棋子数较少一个。并且新产生的两枚棋子中间隔开了
3 个格子。
我们可以不断的进行这样的操作:
1、 如果有相邻的格子都有棋子,则用基本操作2,将他们拿掉,并且放
一颗到右边的格子。
2、 如果某格子棋子>=3,则进行上述操作。
按照这两条规则操作到不能操作为止。此时有棋子的格子,必然是一些互不
相邻的或包含1 颗棋子、或包含2 颗棋子的格子。如果有2 颗棋子,可以这样做:

每次找最右边的2 操作。
如果造成新的“相邻有棋格子”或者“>=3 颗棋子的格子”,我们就能设法
让棋子数至少减1。下面考虑没有造成“相邻有棋格子”或者“>=3 颗棋子的格
子”。
如果红色的1 所在的格子本来就是空的,那么我们就能减少一个含2 的格子。
不然红色1 所在的格子本来必然是1,加了一个红1 后,变成2 了。
这样我们就把最右边的2,往左边移了一点。对新生成的2,继续这样的“移
动”。因为不可能有无穷多个2,所以移到左边的一定位置一定有一个尽头。最
后我们就能把2 的个数减少一个了。
综合上面的说法,我们要不能把棋子数减一,要不能把2 的个数减一。因为
只有有限的棋子和有限的2,总能把棋盘变化到满足题目最终要求的样子。
上面我们证明了解的存在性。与此同时,也给出了一种可行解决方法。利用
对3 和2 的两种基本操作规则不断运用可以求得最后的答案。
实践证明这也是一种不错的方法,程序运行速度很快、编程复杂度也不太高。
但是解的唯一性呢?是不是只有一组解?另外这种方法有很大的偶然性,时
间复杂度不好估计、无法保障。
另外解答给人一种很凌乱的感觉,这促使我寻找更好的方法。
1 1 2
1 1 1
1 1
2
1 1 1
1 1
容易发现,题目给出的两个操作是可逆的。

观察一下上面的图,我们会有这样的感觉:一颗第4 格子的棋子和一颗第5
个格子棋子合起来,等价一颗第6 个格子的棋子。
之所以说“等价”,是因为两者可以互相转化的。通过题目给的第一种操作
可以实现6à4+5;通过第二种,可以实现4+5à6。
我于是产生一种奇妙的想法,何不给每个格子一个量化的值呢?比如设第 i
个格子的价值是Fi,那么Fi 必须满足:Fi+1=Fi+Fi-1。
这不是Fibonacci 数列吗?
如果真能如此,那么在不断变化过程中,所有棋子的价值总和将保持不变。
事实上正是如此。我们可以上面的Sample 来验证一下。
总价值是2*1+8*2+34*1=52

总价值是5+13+34=52
两者的总价值是相等的!
现在的问题是,我们如何利用这一点呢?比如初始状态算出来总价值是52,
我们又怎么知道目标状态的这个52 是如何分布的呢?
因为每个格子只多只能有一个棋子,并且不能存在相邻格子都有棋子;所以
最后结果必然是若干不相邻的Fibonacci 数的和。
把一个数分成若干不邻的Fib 数的和,称之为Fibonacci 分解。
定理 任意自然数n 的Fibonacci 分解唯一。(F1=1, F2=2)
证明:设不大于n 最大Fib 数是Fp,那么Fp必然要包含在分解中。否则:
1
1 1
1 2 3 5 8 13 21 34 55 89
1 1 1
1
1 2 3 5 8 13 21 34 55 89
1 2 1
1
当p 是偶数
Fp-1+Fp-3+…+F5+F3+F1
=Fp-1+Fp-3+…+F5+F3+F2-1
= Fp-1+Fp-3+…+F5+F4-1
……
=Fp-1 #include <iostream> #include<cstring> #include<cmath> using namespace std; long long a[]={0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181, 6765,10946,17711,28657,46368,75025,121393,196418,317811,514229, 832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817, 39088169,63245986,102334155,165580141,267914296,433494437,701408733, 1134903170,1836311903,2971215073,4807526976,7778742049,12586269025, 20365011074,32951280099,53316291173,86267571272,139583862445,225851433717, 365435296162,591286729879,956722026041,1548008755920,2504730781961}; int b[10000]; bool res[500]; int main() { long long n,m,i,temp; cin>>n; while(n--) { long long sum=0,pos=0,apos=0; memset(b,0,sizeof(b)); memset(res,0,sizeof(res)); cin>>m; i=100; pos=2*log((double)m)/log(3.0)+5; while(m>0) { cin>>b[i]; m -=b[i]; if(apos==0 && b[i] !=0) apos=i; i++; } pos = apos - pos; apos=i; i=1; while(pos<=apos) { sum += b[pos]*a[i]; i++; pos++; } temp=sum; i=61; while(sum>0) { if(sum>=a[i]) { res[i]=1; sum -=a[i]; } i--; } i=1; bool flag=0; while(temp>0) { if(res[i]) {flag=1;temp -=a[i];} if(flag) cout<<res[i]<<" "; i++; } cout<<endl; } return 0; }

四、 上楼梯问题
例3 Conqueror’s battalion(CEOI2002,day1,problem1) 题目大意如下:
总共有n 阶台阶,一些台阶上有你的若干士兵。你要把所有的士兵分成两组,
然后敌人就会告诉你哪一组士兵留下,哪一组士兵被消灭。那些留下的士兵上一
层楼。
然后你重新把剩下的士兵分组,敌人再次选择一组留下;留下的士兵又上一
层楼。
……
如是反复。如果最后有一个士兵登顶,也就是上了第n 层台阶,你就赢了;
如果士兵全部被消灭完,敌人赢了。
现在输入n 和每一层台阶上有多少你的士兵。你的任务是判断你能否获胜;
如果能获胜,还必须和库文件交互,由库文件充当敌人,你要通过适当的分组,
最终获得胜利。
下面分析这个题目。
设身处地的想,如果你是敌人,你怎么办?对方把士兵分成两组,你会杀掉
哪一组?是不是人数最多的那一组?不是。
比如第n-1 层上站了1 个人,他单独作为一组;第1 层上占了100 个人,他
们是一组。你会杀谁?
当然是第n-1 层上的那个人。因为如果不杀,下一轮,他就上到第n 层、对
方就获胜了!
可见,站在第n-1 层的人比站在第1 层的人,更具有威胁性——或者说,短
期内,能造成更大的危害。
一般的,站得越高,危险性越大。这里提示我们士兵的地位是不同的,不能
以人数的多少来作为判定的依据。
回到我们自己,看看如何才能赢得胜利。
设想只有2 个人站在第i 层台阶。
你绝不可能把他们分到一组;否则敌人下一轮就能让他们两个全部玩完。所
以肯定是两个人分别在一组。
敌人让某个人死亡后,剩下的那个就能上到第i+1 层台阶了。

也就是说:站在第i 层上的2 个人,其作用相当于站在第i+1 层上的1 个人。
类似的模仿例2,我们令Wi表示站在第i 层上的人的价值,那么2Wi=Wi+1。
令W0=1,则Wi=2i。
最后的目标是要让一个人站在第n阶上,也就是目标状态的价值和至少是2n。
设想初始状态的总价值也是2n。我们分组的时候就设法分成两组,使得每一
组的总价值和都是2n-1,这样无论敌人去掉哪一组,剩下的那组价值和始终是2n-1。
由于剩下的士兵要上爬一层,相当于每个人*2,总价值和变成了2n。
所以在博弈的过程中,总价值和是守恒的:始终保持为2n!由于人数不断减
少,最后只剩下一个人的时候,必然已经登顶了。

反之如果总价值<2n,分成两组后,必然有一组<2n-1,敌人可以把该组保留。 该组士兵上爬一层,总价值<2n。变化过程中总价值始终<2n,也就是说无论如何 也无法登顶。 如果总价值>2n,总能分成两组,使得都>=2n-1。这样变化过程中总价值就始
终>=2n了。人数不断减少,最后总有一个人能登顶。
具体怎么分,可以采用从大到小的贪心,这里不详细说了。因为和本文主题
无关。这里只想要重点介绍这种标权值的方法。

判断能不能取胜,转化成了判断总价值和是否>=2n。如果>=2n,我们就能想
方设法的分组,使得总价值一直保持在2n之上;随着人数的减少最后取胜。
“总价值始终保持在2n之上”这就是本题的守恒。严格的说他已经不是“等
于”关系了,而是一种大于关系。算是广义的守恒吧。
但他的基本思想还是找变化中的不变量。用二进制标记个层阶梯是一个很好
的尝试,这种尝试也是建立在仔细观察和联想的基础上的。结合题目的特点,找
变化中的不变量!

【总结】
以上的几个题目,本身都有一定隐蔽性,看不出什么噱头。只有通过一定的
转化,具体地说比如求和、标权值,才能发现其本质。
例1 为什么求和?例2 为什么是Fibonacci 权?例3 怎么又是二进制?再一
次强调:
问题本身的结构是守恒量选择的主要依据。
联想和化归是构造守恒量的关键。
观察和联想是最关键的。观察是观察数据的规律、规则的特点等等;联想是
在观察到的特点基础上,与已有的知识储备进行联系,或者自行创造。
知识的积累是很重要的。否则即便是正确的归纳到了特点,也无法有效的类
比创造。
另外试图用“守恒法”解题的话,创造性的思维是很重要的。有很多守恒量
并不能在题目中找到,必须自己创造。有一个经典的例子是Fibonacci Heap。
Fib Heap 本身实现没有用到守恒法,但是分析复杂度的时候用到了“势能
法”。该方法通过构造一个函数,证明了 Fib heap 删除操作的均摊复杂度是
O(logn)。这也算是守恒思想的一个了不起的应用。

其实“守恒”随处可见。你不经意的列一个方程就是守恒。本文说的守恒是
说在题目中挑大梁、演主角的一种思维方法和解决问题的算法。
大概任何人早就有意无意的多次用到了这种思想,本文只是想将这种方法的
主要特点进行归纳,并且着重提出,引起注意。以后在遇到问题的时候,可以有
意识的往这方面联想,或许能有意向不到的收获。
【参考文献】
CEOI 试题——例3
POI 试题——例2
雅礼内部测试题——例1

【附录】
例2 的原题:
POI IV Stage I Problem 2
Jumps
"Jumps" is a board game played on an infinite tape of squares. The board has neither
left nor right limit. On the squares there is finitely many pieces (more than one piece
on a square is allowed). We assume that the most left, non-empty square is numbered
0. Squares on the right are denoted by consecutive natural numbers 1, 2, 3, ..., squares
on the left - by negative numbers: -1, -2, -3, ... . The configuration of pieces on the
board can be described in the following way: for every square occupied by at least
one piece we give the number of the square and the number of pieces standing on this
square. There are two kinds of moves that can change the configuration: jump right
and jump left. Jump right consists of removing two pieces from the board: one from a
square p and the other from the square p+1, and placing one piece on the square p+2.
Jump left consists of removing a piece from a square p+2 and placing two pieces on
the board: one on the square p+1 and the other on the square p. We say that a
configuration is final if each pair of neighbouring squares contains at most one piece.
For every configuration there is exactly one final configuration that can be reached
after finite number of moves (jumps).
Task

Write a program that:
· reads a description of an initial configuration from the text file SKO.IN,
· finds the final configuration that can be reached from the given initial
configuration and writes the result to the text file SKO.OUT.
Input
In the first line of the file SKO.IN there is one positive integer n, 1 <= n <= 10000, which equals the number of non-empty squares of the given initial configuration. In each of the following n lines there is a description of one non-empty square of the initial configuration. Such a description consists of two integers. First is the number of the square, second - the number of pieces standing on this square. The descriptions are given in increasing order with respect to numbers of squares. The biggest number of a square is not greater than 10000 and the number of pieces on a single square is not greater than 100000000. Output In the first line of the file SKO.OUT there should be written the final configuration that can be reached from the given initial configuration. More precisely the line should contain the numbers of non-empty squares (in increasing order) of the final configuration. The numbers should be separated by single spaces. Example For the text file SKO.IN 2 0 5 3 3 the correct solution is the file SKO.OUT -4 -1 1 3 5 例3 的原题: Conquer 试题 有两个人在玩一种叫“征服”游戏,其中征服者一方在最初拥有最多1 000 000 000 名士兵。这些士兵站在N高地(2<=N<=2000),每个高地上有若干士兵。征服 者可以按任意方式将高地上的士兵分成两队,被征服的一方可以叫任何一队离 开,留下的士兵将顺次占领上一级高地。如果征服者一方最终至少有一名士兵到 达最高点,征服者就获胜,否则他应该继续这个游戏,直到没有士兵时就失败了。 这是一个交互式的问题。测试者将提供给你一个初始库,假设你为征服者, 你将依靠这个库进行游戏,每一轮游戏,你需要提供给库一个分组方案。库将根 据你的提供,返回 1 或 2 (1 表示留下你描述给库的那队士兵, 2 表示留下其余 的士兵).如果你获胜或者没有士兵进行游戏时,则游戏结束,库将自动终止你的 程序,你不能用任何方式进行终止。 库接口 库 libconq 提供两个子程序: · start – 返回 N 和一个数组stairs 的数值,表示每个高地的士兵数目 (即 stairs[i] 为高地i的士兵数,其中stairs[1]为最高点) · step – 提供给它一个入口数组参数subset (有 N 个元素),描述了一次士兵 分队的方案。它将返回 1 或 2(含义见上); 如果你提供了一个无效的分组方式,库将自动终止你的程序,并得0分。 下面是FreePascal 和 C/C++对子程序的描述: procedure start(var N: longint; var stairs:array of longint); function step(subset:array of longint): longint; void start(int *N, int *stairs);1 int step(int *subset); 下面给出了一个FreePascal 和 C/C++关于库使用的样例; 程序包含了两个部 分:开始游戏,随机选择某一队士兵。你的程序将可能包含每一轮游戏,循环进 行。你可以样例的方式定义数组。(注意在返回时FreePascal 可以按定义的方式, 而C/C++只能是从1 到N) FreePascal example uses libconq; var stairs: array[1..2000] of longint; subset: array[1..2000] of longint; i ,N,result: longint; ... start(N,stairs); ... for i:=1 to N do if random(2)=0 then subset[i]:=0 else subset[i]:=stairs[i]; result:=step(subset); ... C/C++ example #include "libconq.h" int stairs[2001]; int subset[2001]; int i,N,result; ... start(&N, stairs); ... for (i=1;i<=N;i++) if (rand()%2==0) subset[i]=0; else subset[i]=stairs[i]; result=step(subset); ... 你必须在你的程序使用uses libconq 进行库的连接(FreePascal)而在C/C++中使 用#include "libconq.h" 形式。这时,程序将对加入的libconq.c 中的参数一起进行 编译. 样例 You Library start(N,stairs) N=8, stairs=(0,1,1,0,3,3,4,0) Step((0,1,0,0,1,0,1,0)) returns 2 Step((0,1,0,0,0,1,0,0)) returns 2 Step((0,0,0,3,2,0,0,0)) returns 1 Step((0,0,2,0,0,0,0,0)) returns 2 Step((0,1,0,0,0,0,0,0)) returns 2 Step((0,1,0,0,0,0,0,0)) no return: you won 资源 你可以先建立一个libconq.dat 文件, 该文件包含两行,第一行为N,表示高地数, 接下来的一行包含N个整数,分别表示高地 1..N 中的士兵数目,当然最开始高 地1不会有士兵. libconq.dat 8 0 1 1 0 3 3 4 0

我猜你可能也喜欢:

No Comments - Leave a comment

Leave a comment

电子邮件地址不会被公开。 必填项已用*标注

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>


Welcome , today is 星期六, 2017 年 10 月 21 日