﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-ickchen2-文章分类-ACM文章</title><link>http://www.cppblog.com/ickchen2/category/7950.html</link><description /><language>zh-cn</language><lastBuildDate>Mon, 06 Oct 2008 08:56:58 GMT</lastBuildDate><pubDate>Mon, 06 Oct 2008 08:56:58 GMT</pubDate><ttl>60</ttl><item><title>利用块移动求逆序</title><link>http://www.cppblog.com/ickchen2/articles/63307.html</link><dc:creator>神之子</dc:creator><author>神之子</author><pubDate>Mon, 06 Oct 2008 04:52:00 GMT</pubDate><guid>http://www.cppblog.com/ickchen2/articles/63307.html</guid><wfw:comment>http://www.cppblog.com/ickchen2/comments/63307.html</wfw:comment><comments>http://www.cppblog.com/ickchen2/articles/63307.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ickchen2/comments/commentRss/63307.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ickchen2/services/trackbacks/63307.html</trackback:ping><description><![CDATA[<p>对数列的一次&#8220;块移动&#8221;是指把一段数取出来插入到数列中的另一个地方（说穿了就是一次选择剪切粘贴的操作）。例如，数列1,4,5,6,2,3,7可以通过一次块移动完成排序（把456挪到3后面）。那么，想要让一个1到n的逆序排列n, n-1, ..., 3, 2, 1变为顺序排列，最少需要多少次块移动？给出你的算法，并证明这个移动数目不能再少了。</p>
<p>需要指出的是，答案并不是n-1那么简单。当n=5时，只需要三步就可以搞定了：</p>
<p>5 4 [3 2] 1<br>3 2 5 [4 1]<br>[3 4] 1 2 5<br>1 2 3 4 5</p>
<p><br>事实上，给出1到n的逆序排列，最少需要Ceil[(n+1)/2]次块移动就可以完成排序（除了n=1或n=2，Ceil表示取上整）。当n为奇数时，一个满足要求的算法是：每一次把数字n后面那一段的正中间两个元素拿出来，插入到数字n前面那一段数的正中间。当数字n后面的数被移动完了后，把它前面n-1个数左右两半对换一下就行了。例如，当n=7时：</p>
<p>7 6 5 [4 3] 2 1<br>4 3 7 6 [5 2] 1<br>4 5 2 3 7 [6 1]<br>[4 5 6] 1 2 3 7<br>1 2 3 4 5 6 7</p>
<p>算法的移动步数显然为(n+1)/2，其正确性可以用数学归纳法说明，这里不再详细叙述了。<br>当n为偶数时，只需要用n/2次操作把前面n-1个元素排好序，再花一次操作把末一个元素移动到最前面，加起来正好Ceil[(n+1)/2]次操作。下面我们证明，移动次数不可能比Ceil[(n+1)/2]更少。<br>对于数列中相邻的两个数，如果前面那个数比后面的大，我们就把它们俩称作一组&#8220;逆序相邻数&#8221;。初始时，数列中有n-1个这样的逆序相邻数，我们的目标就是通过块移动把这个数目减少到0。整个证明过程的关键就在于，一次块移动操作最多只能消除两个逆序相邻数。</p>
<p>原数列： **aA--Bb***CD****<br>新数列： **ab***CA--BD****</p>
<p>假如我们把块A--B插入到CD中间。注意到，相邻数发生变动的地方只有三处。要想同时消除三个逆序相邻数，只有一种可能：原数列中a&gt;A, B&gt;b, C&gt;D，同时新数列中的a&lt;b, C&lt;A, B&lt;D。这将导出一个很荒谬的结论：A &lt; a &lt; b &lt; B &lt; D &lt; C &lt; A。这告诉我们，一次块移动同时消除三个逆序相邻数是不可能的，它最多只能消除两个逆序相邻数。另外，容易看出，第一次移动只能消除一个逆序的相邻数，因为初始时原数列完全逆序，即有a &gt; A &gt; B &gt; b &gt; C &gt; D，在新数列中只有C&lt;A成立。对称地，最后一次移动也只可能消除一个逆序相邻数，因为新数列中a &lt; b &lt; C &lt; A &lt; B &lt; D，只有B&gt;b是成立的。<br>于是我们得知，k次块移动最多消除1+2*(k-2)+1个逆序相邻数。为了消除n-1个逆序相邻数，我们有1+2*(k-2)+1 &gt;= n-1，整理得k&gt;=(n+1)/2。</p>
<img src ="http://www.cppblog.com/ickchen2/aggbug/63307.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ickchen2/" target="_blank">神之子</a> 2008-10-06 12:52 <a href="http://www.cppblog.com/ickchen2/articles/63307.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>约瑟夫问题</title><link>http://www.cppblog.com/ickchen2/articles/63181.html</link><dc:creator>神之子</dc:creator><author>神之子</author><pubDate>Fri, 03 Oct 2008 03:23:00 GMT</pubDate><guid>http://www.cppblog.com/ickchen2/articles/63181.html</guid><wfw:comment>http://www.cppblog.com/ickchen2/comments/63181.html</wfw:comment><comments>http://www.cppblog.com/ickchen2/articles/63181.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ickchen2/comments/commentRss/63181.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ickchen2/services/trackbacks/63181.html</trackback:ping><description><![CDATA[<p class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><span style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">约瑟夫问题（转）</span></p>
<p><font face=宋体>无论是用链表实现还是用数组实现都有一个共同点：要模拟整个游戏过程，不仅程序写起来比较烦，而且时间复杂度高达<span lang=EN-US>O(nm)</span>，当<span lang=EN-US>n</span>，<span lang=EN-US>m</span>非常大<span lang=EN-US>(</span>例如上百万，上千万<span lang=EN-US>)</span>的时候，几乎是没有办法在短时间内出结果的。我们注意到原问题仅仅是要求出最后的胜利者的序号，而不是要读者模拟整个过程。因此如果要追求效率，就要打破常规，实施一点数学策略。</font></p>
<p><font face=宋体>为了讨论方便，先把问题稍微改变一下，并不影响原意：</font></p>
<p><font face=宋体>问题描述：<span lang=EN-US>n</span>个人（编号<span lang=EN-US>0~(n-1))</span>，从<span lang=EN-US>0</span>开始报数，报到<span lang=EN-US>(m-1)</span>的退出，剩下的人继续从<span lang=EN-US>0</span>开始报数。求胜利者的编号。</font></p>
<p><font face=宋体>我们知道第一个人<span lang=EN-US>(</span>编号一定是<span lang=EN-US>m%n-1) </span>出列之后，剩下的<span lang=EN-US>n-1</span>个人组成了一个新的约瑟夫环（以编号为<span lang=EN-US>k=m%n</span>的人开始）</font><font face=宋体><span lang=EN-US>:<br>&nbsp;&nbsp; k&nbsp;&nbsp; k+1&nbsp;&nbsp; k+2&nbsp;&nbsp; ... n-2, n-1, 0, 1, 2, ... k-2<br></span>并且从<span lang=EN-US>k</span>开始报<span lang=EN-US>0</span>。</font></p>
<p><font face=宋体>现在我们把他们的编号做一下转换：</font><span lang=EN-US><br><font face=宋体>k&nbsp;&nbsp;&nbsp;&nbsp; --&gt; 0<br>k+1 --&gt; 1<br>k+2 --&gt; 2<br>...<br>...<br>k-2 --&gt; n-2<br>k-1 --&gt; n-1</font></span></p>
<p><font face=宋体>变换后就完完全全成为了<span lang=EN-US>(n-1)</span>个人报数的子问题，假如我们知道这个子问题的解：例如<span lang=EN-US>x</span>是最终的胜利者，那么根据上面这个表把这个<span lang=EN-US>x</span>变回去不刚好就是<span lang=EN-US>n</span>个人情况的解吗？！！变回去的公式很简单，相信大家都可以推出来：<span lang=EN-US>x'=(x+k)%n</span></font></p>
<p><font face=宋体>如何知道<span lang=EN-US>(n-1)</span>个人报数的问题的解？对，只要知道<span lang=EN-US>(n-2)</span>个人的解就行了。<span lang=EN-US>(n-2)</span>个人的解呢？当然是先求<span lang=EN-US>(n-3)</span>的情况<span lang=EN-US> ---- </span>这显然就是一个倒推问题！好了，思路出来了，下面写递推公式：</font></p>
<p><font face=宋体>令<span lang=EN-US>f[i]</span>表示<span lang=EN-US>i</span>个人玩游戏报<span lang=EN-US>m</span>退出最后胜利者的编号，最后的结果自然是<span lang=EN-US>f[n]</span></font></p>
<p><font face=宋体>递推公式</font><span lang=EN-US><br><font face=宋体>f[1]=0;<br>f[i]=(f[i-1]+m)%i;&nbsp;&nbsp; (i&gt;1)</font></span></p>
<p><font face=宋体>有了这个公式，我们要做的就是从<span lang=EN-US>1-n</span>顺序算出<span lang=EN-US>f[i]</span>的数值，最后结果是<span lang=EN-US>f[n]</span>。因为实际生活中编号总是从<span lang=EN-US>1</span>开始，我们输出<span lang=EN-US>f[n]+1</span></font></p>
<p><font face=宋体>由于是逐级递推，不需要保存每个<span lang=EN-US>f[i]</span>，程序也是异常简单：</font></p>
<p><span lang=EN-US><font face=宋体>#include &lt;stdio.h&gt;</font></span></p>
<p><span lang=EN-US><font face=宋体>main()<br>{<br>&nbsp;&nbsp; int n, m, i, s=0;<br>&nbsp;&nbsp; printf ("N M = "); scanf("%d%d", &amp;n, &amp;m);<br>&nbsp;&nbsp; for (i=2; i&lt;=n; i++) s=(s+m)%i;<br>&nbsp;&nbsp; printf ("The winner is %d\n", s+1);<br>}</font></span></p>
<p><font face=宋体>这个算法的时间复杂度为<span lang=EN-US>O(n)</span>，相对于模拟算法已经有了很大的提高。算<span lang=EN-US>n</span>，<span lang=EN-US>m</span>等于一百万，一千万的情况不是问题了。可见，适当地运用数学策略，不仅可以让编程变得简单，而且往往会成倍地提高算法执行效率。</font></p>
<p><font face=宋体>提示：<span lang=EN-US>m=2</span>约瑟夫问题在<span lang=EN-US>Knuth</span>的著作《具体数学》中有介绍，它采用了一种更高效的方法，可以用一个代数式来表示结果。类似的我已经推出<span lang=EN-US>m&gt;2</span>的递推公式情形，不过还没办法转换成单个代数式，而且程序实现起来可能比较麻烦，所以只好作罢。</font></p>
<p class=MsoNormal style="MARGIN: 0cm 0cm 0pt"><span lang=EN-US>*</span><span style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">改进的递推公式</span><span lang=EN-US><br>(1)i&lt;m</span><span style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">时</span><span lang=EN-US> f[i]=(f[i-1]+m)%i;<br>(2)i&gt;=m</span><span style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">时</span> <span style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">令</span><span lang=EN-US>i=km+r (0&lt;=r&lt;m) </span><span style="FONT-FAMILY: 宋体; mso-ascii-font-family: 'Times New Roman'; mso-hansi-font-family: 'Times New Roman'">则</span><span lang=EN-US> f[i]=f[km+r]=f[k(m-1)+r]-r+floor((f[k(m-1)+r]-r-1)/(m-1));</span></p>
<img src ="http://www.cppblog.com/ickchen2/aggbug/63181.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ickchen2/" target="_blank">神之子</a> 2008-10-03 11:23 <a href="http://www.cppblog.com/ickchen2/articles/63181.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>求逆序对~</title><link>http://www.cppblog.com/ickchen2/articles/62422.html</link><dc:creator>神之子</dc:creator><author>神之子</author><pubDate>Sun, 21 Sep 2008 14:54:00 GMT</pubDate><guid>http://www.cppblog.com/ickchen2/articles/62422.html</guid><wfw:comment>http://www.cppblog.com/ickchen2/comments/62422.html</wfw:comment><comments>http://www.cppblog.com/ickchen2/articles/62422.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/ickchen2/comments/commentRss/62422.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/ickchen2/services/trackbacks/62422.html</trackback:ping><description><![CDATA[<p align=left>目前我知道的求逆序最快的适合ACM/ICPC的算法是归并排序时计算逆序个数，时间复杂度是nlog2n，而空间复杂度2n。</p>
<p align=left><font color=#0000ff>归并求逆序简单原理：</font><br>归并排序是分治的思想，具体原理自己去看书吧。利用归并求逆序是指在对子序列 s1和s2在归并时，若s1[i]&gt;s2[j]（逆序状况），则逆序数加上s1.length-i,因为s1中i后面的数字对于s2[j]都是逆序的。</p>
<p align=left><font color=#0000ff>TJU 2242:</font><br>直接上模板，记得m的奇偶要考虑的哦。</p>
<p align=left><font color=#0000ff>PKU 1007:</font><br>求逆序数，然后排序输出就行了。</p>
<p align=left><font color=#0000ff>PKU 1804, PKU 2299:<br></font>是最简单的关于逆序对的题目，题目大意是给出一个序列，求最少移动多少步可能使它顺序，规定只能相邻移动。<br>相邻移动的话，假设a b 相邻，若a&lt;b 交换会增加逆序数，所以最好不要做此交换；若a==b 交换无意思，也不要进行此交换；a&gt;b时，交换会减少逆序，使序列更顺序，所以做交换。<br>由上可知，所谓的移动只有一种情况，即a&gt;b，且一次移动的结果是逆序减1。假设初始逆序是n，每次移动减1，那么就需要n次移动时序列变为顺序。所以题目转化为直接求序列的逆序便可以了。</p>
<p align=left><font color=#0000ff>ZJU 1481:</font><br>这题和本次预选赛的F略有相似，不过要简单得多。题意是给定序列s，然后依次将序列首项移至序列尾，这样共有n-1次操作便回到了原序列（操作类似于循环左移）。问这n-1次操作和原序列，他们的逆序数最小的一次是多少？<br>有模板在手，直观地可以想到是，对于这n次都求逆序数，然后输出最小的一次就可以了，但这样做的复杂度有O(n*nlogn),太过复杂。<br>如果只求初始序列的逆序数的话，只要后面的n-1次操作的逆序数能够在O(1)的算法下求得，就能保证总体O(nlogn)的复杂度了。事实上，对于每次操作确实可以用O(1)的算法求得逆序数。将序列中ai移到aj的后面，就是ai做j-i次与右邻的交换，而每次交换有三个结果：逆序+1、逆序-1、逆序不变。由于题目中说明序列中无相同项，所以逆序不变可以忽略。逆序的加减是看ai与aj间（包括aj）的数字大小关系，所以求出ai与aj间大于ai的数字个数和小于ai的数字个数然后取差，就是ai移动到aj后面所导致的逆序值变化了。<br>依据上面的道理，因为题目有要求ai是移动到最后一个数，而ai又必定是头项，所以只要计算大于ai的个数和小于ai的个数之差就行了。然后每次对于前一次的逆序数加上这个差，就是经过这次操作后的逆序数值了。</p>
<p align=left><font color=#0000ff>PKU 2086:</font><br>这题不是求逆序对，而是知道逆序数k来制造一个序列。要求序列最小，两个序列比较大小是自左向右依次比较项，拥有较大项的序列大。 <br>其实造序列并不难，由1804可知，只要对相邻数做调整就能做到某个逆序数了。难点是在求最小的序列。举例 1 2 3 4 5,要求逆序1的最小序列是交换4 5，如果交换其他任意相邻数都无法保证最小。由此可以想到，要保证序列最小，前部分序列可以不动（因为他们已经是最小的了），只改动后半部分。而我们知道n个数的最大逆序数是n*(n-1)/2，所以可以求一个最小的p，使得 k&lt;p*(p-1)/2。得到前半部分是1到n-p，所有的逆序都是由后半部分p个数完成的。<br>考虑k=7,n=6的情况，求得p=5,即前部分1不动，后面5个数字调整。4个数的最大逆序是5 4 3 2,逆序数是6，5个数是6 5 4 3 2,逆序数是10。可以猜想到，保证5中4个数的逆序不动，调整另一个数的位置就可以增加或减少逆序数，这样就能调整出6-10间的任意逆序。为了保证最小，我们可以取尽量小的数前移到最左的位置就行了。2前移后逆序调整4，3前移后调整了3，4调整2，5调整1，不动是调整0，可以通过这样调整得到出6-10，所以规律就是找到需要调整的数，剩下的部分就逆序输出。需要调整的数可以通过总逆序k-(p-1)*(p-2)/2+(n-p)求得。</p>
<p align=left><font color=#0000ff>PKU 1455:</font><br>这是一道比较难的关于逆序数推理的题目，题目要求是n人组成一个环，求做相邻交换的操作最少多少次可以使每个人左右的邻居互换，即原先左边的到右边去，原右边的去左边。容易想到的是给n个人编号，从1..n，那么初始态是1..n然后n右边是1，目标态是n..1，n左边是1。<br>初步看上去好象结果就是求下逆序（n*(n-1)/2 ?），但是难点是此题的序列是一个环。在环的情况下，可以减少许多次移动。先从非环的情况思考，原1-n的序列要转化成n-1的序列，就是做n(n-1)/2次操作。因为是环，所以(k)..1,n..k+1也可以算是目标态。例如：1 2 3 4 5 6的目标可以是 6 5 4 3 2 1,也可以是 4 3 2 1 6 5。所以，问题可以转化为求形如(k)..1,n..k+1的目标态中k取何值时，逆序数最小。<br>经过上面的步骤，问题已经和ZJU1481类似的。但其实，还是有规律可循的。对于某k，他的逆序数是左边的逆序数+右边的逆序数，也就是(k*(k-1)/2)+((n-k)*(n-k-1)/2) （k&gt;=1 &amp;&amp; k&lt;=n）。展开一下，可以求得k等于n/2时逆序数最小为((n*n-n)/2)，现在把k代入进去就可以得到解了。<br>要注意的是k是整数，n/2不一定是整数，所以公式还有修改的余地，可以通用地改为(n/2)*(n-1)/2。</p>
<p align=left><font color=#0000ff>PKU 2893:</font><br>用到了求逆序数的思想，但针对题目还有优化，可见<a href="http://blog.csdn.net/ray58750034/archive/2006/09/05/1177375.aspx"><strong><font color=#006bad>M*N PUZZLE的优化</font></strong></a>。</p>
<p align=left><font color=#0000ff>PKU 1077:</font><br>比较经典的搜索题，但在判断无解的情况下，逆序数帮了大忙，可见<a href="http://blog.csdn.net/ray58750034/archive/2006/02/15/599897.aspx"><strong><font color=#006bad>八数码实验报告</font></strong></a>。</p>
<img src ="http://www.cppblog.com/ickchen2/aggbug/62422.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/ickchen2/" target="_blank">神之子</a> 2008-09-21 22:54 <a href="http://www.cppblog.com/ickchen2/articles/62422.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>