Description:
一个01串,我们可以对相邻两个字符实行交换位置的操作. 求最少的操作次数使得所有的1的位置连续. eg. s="010110",swap(s[1],s[2])之后,变成"001110". 所以答案是1.Input:
多组数据,每组数据一个01字符串.串长不超过10^5Output:
首先输出Case #k: ,然后是答案.Sample Input:
010110 10101100 000
Sample Output:
Case #1: 1 Case #2: 3 Case #3: 0
Source:
dd
题目链接:http://acm.zjut.edu.cn/ShowProblem.aspx?ShowID=1703
---------------------分割线--------------------------
/* -----------------------------------------------
这一题还可以用回溯法来做,这是我第一次的解法。因为要回溯求出所有解,然后再求出其中一个最优解,
所以时间复杂度相当高,为指数时间O(2^(n-1)),结果当然就是Time Limit Exceed。
分析:
例如01串:01001110010001,也是先找出所有连续的1字串,1,111,1,1。不过这一个解法和之前的有所不同。
大概想法是这样:对于两个连续的1字串来说,他们合并的方式有两种,1种就是左边1字串向右移动,1种就是右边
的字串向左移动。这里的移动必然会产生不同的代价,除了特殊情况以外。例如,对于,,,100111..,左边向右合并
会产生2步的代价,合并后会变成...1111...,而右边向左边移动则会产生6步的代价,合并后会变成...111100..
这样一看,当然是前者用的步数少,其实不然。合并之后还要继续和第三组1合并,然而根据和第三组合并的代价来看,
前者未必是最优的。所以这里就有一个涉及到动态决策的问题,是向左合并还是向右合并呢?
对于这种问题,用回溯法搜索出所有解然后求出最优解是可行的,这里向左向右的决策将会产生一棵叶子数为2^n-1
的完全二叉树,要搜索出所有解就一定要搜索到叶子,有2^(n-1)的分支,所以时间复杂为O(2^(n-1)),效率相当的
低。
此解法还有一些后续问题。。。。见代码之后
----------------------------------------------- */
#include<stdio.h>
char szStr[100005] ;
int FindMinStep(int nOneSum, int nZeroSum , int nIndex) ;
int main(void)
{
int fFindOne = 0 ;
int fFindZero = 0 ;
int nMinToLeft = 0 ;
int nMinToRight = 0 ;
int nMinStep = 0 ;
int nOneFwSum = 0 ; //0前面的1,比如110001,指的是11
int nOneBhSum = 0 ; //0后面的1,比如11000111,指的是111
int nZeroSum = 0 ;
int i = 0 ;
int j = 1 ;
while(scanf("%s",szStr) != EOF)
{
fFindOne = fFindZero = -1 ;
nMinToLeft = nMinToRight = nMinStep = 0 ;
nOneFwSum = nOneBhSum = nZeroSum = 0 ;
i = 0 ;
while(szStr[i] != '\0')
{
if('1' == szStr[i])
{
fFindOne = 1 ;
if(fFindZero > 0)
{
nOneBhSum++ ;
if('0' == szStr[i+1] || '\0' == szStr[i+1])
{
nMinToLeft = nOneBhSum*nZeroSum + FindMinStep(nOneFwSum+nOneBhSum,nZeroSum,i+1) ; //向左决策
nMinToRight = nOneFwSum*nZeroSum + FindMinStep(nOneFwSum+nOneBhSum,0,i+1) ; //向右决策
nMinStep = nMinToLeft < nMinToRight ? nMinToLeft : nMinToRight ; //左右分支中最少步数
break ;
}
}
else
{
nOneFwSum++ ;
}
}
else if('0' == szStr[i])
{
if(fFindOne > 0 )
{
nZeroSum++ ;
fFindZero = 1 ;
}
}
i++ ;
}
printf("Case #%d: %d\n",j,nMinStep) ;
j++ ;
}
return 0 ;
}
//参数的意义分别是:此时已经合并1的数目、与下一个1串相隔多少个0、此时在01串的下标
int FindMinStep(int nOneSum, int nZeroSum , int nIndex)
{
int i = nIndex ;
int fFindZero = 1 ;
int nMinToLeft = 0 ;
int nMinToRight = 0 ;
int nMinStep = 0 ;
int nOneFwSum = nOneSum ;
int nOneBhSum = 0 ;
int nZeroThisSum = nZeroSum ;
if('\0' == szStr[nIndex])
{
return 0 ;
}
else
{
while(szStr[i] != '\0')
{
if('1' == szStr[i])
{
nOneBhSum++ ;
if('0' == szStr[i+1] || '\0' == szStr[i+1])
{
nMinToLeft = nOneBhSum*nZeroThisSum + FindMinStep(nOneFwSum+nOneBhSum,nZeroThisSum,i+1) ; //继续向左决策
nMinToRight = nOneFwSum*nZeroThisSum + FindMinStep(nOneFwSum+nOneBhSum,0,i+1) ; //继续向右决策
nMinStep = nMinToLeft < nMinToRight ? nMinToLeft : nMinToRight ;
return nMinStep ; //回溯
}
}
else if('0' == szStr[i])
{
nZeroThisSum++ ;
}
i++ ;
}
}
return nMinStep ; //回溯
}
/* -------------------------------------------
回溯法解法后续问题:
分析:
那个回溯法遍历产生完全二叉树的所有分支其实有一半是和所有1串全部向右移的情况一样的,这里指的
一样,并不是指步数一样,而是指1串的最终停留的位置一样。产生这种情况的前提是,我的算法是从左
往右遍历的,因为算法是从01串的左往右进行合并,所以最后一个决策肯定是针对已经合并的1串和最右
边也就是最后一个1串的合并。如果最后一个决策是已经合并串的向右移动,那就是说所有1串都往最右边
靠了。否则则是最后一个1串往左边合并。
例如001110111001,所有1串向右移的情况有两种,第一种移法就是所有1串直接向右移,第二种做法就
是中间的1串先向左,再向右。这两种移法最终结果都一样,但是步数却不一样,前者肯定是向右时产
生最少步数的,而后者先向左再向右这一种移法,中间的1串重复移动了,所以结果无论相对于全部向右移,
或者其它移动方案来说都不是最优。
现在回到刚才提到遍历产生的完全二X树,所有包含右叶子的那些分支都和全部1串向右移产生的情况一
样(在纸上画画就可以看出来了),然而只有所有决策都是向右移的情况这是最优的。所以这些分支都是
无用且浪费时间的。
这里可以引申出一个结论:
在这一棵遍历二X树中,如果一开始的决策是向右的,在后面的决策中,只允许产生一次变向决策的(也就是
变向左移动)。这一个变向的决策,其实就是找到了线性解法中的固定点,这是一个转折点。在这个转折点之
后所有的1串都要向左移动。如果在变向之后,在后续的决策中还要再产生一个变向决策(向右决策),则证明
之前做的一个变向决策所找到的固定点不会产生最优解,这个最优解必然在最后一个的变向决策中。
所以,对于回溯的优化可以在这里下手。
大概如下:
1、先记下一路向左移动所产生的步数。
2、从第一个向右移动开始决策,记录步数。然后在遇到第一个变向决策时,设置已经变向的标志位。
3、然后继续一边记录向左决策产生的步数,一边记录一直向右决策产生的步数(产生第二次变向决策时有用)。
4、如果在后续决策中没有产生第二次变向,则这一个就是最优解。
5、如果在后续决策中产生了第二次变向,刚跳到从这个变向点开始遍历,而第二步储存的一路向右产生的步数
就变成了假设在决策到这一个变向点之前所产生的最少步数。
6、然后继续决策,如果遇到变向决策,则回到了第2、3步,如果此后没有再遇到变向决策,则这一分支其实就是
所有1串一路向右移动的情况。
思路大概就是这样,其实就是减少了无用分支,取消了回溯这两步。
这样就可以将回溯法变为线性算法o(n),n为1串的数目。
代码就等考完试再实现~~~~
------------------------------------------- */
-----------------------分割线---------------------
/* ---------------------------------------------------------------------------------------------
分析:
1、最少步数将所有1的位置连续,这应该是个最优化问题。嗯,先想到的是用动态规划法。但是......我
写不出来....现在还在纠结能不能用动态规划.......状态位移方程也写不出来。
2、首先要找出01串里面的所有连续的1字串,例如01串:01001110010001,这里面的所有连续的1字串有,
1,111,1,1,总共有4连续的1字串。然后我是这样想的(错了N次之后),要做到最少步数,就必须做到最
少次数去移动那些连续1字串或者最少次数去移动那些连续的0。因为题目是针对1,所以我选择去移动
连续的1字串。
对于串01001110010001,这里总共有4个连续1字串,而无论怎样去移动,程序的最终结果都是所有的1
放在一起。所以,对于4个连续1字串来说,只要移动其中的3个字串就可以了。同理对于N个连续的1字串
,只需移动其中的N-1个的字串就可以做到最少步数。
3、所以整体的思路应该是这样:
1、先找出所有连续的1字串,然后每一次只固定一个连续的1字串,其余的1字串则向这个固定的中心移动,
统计步数,然后比较移向不同中心时所产生的步数,求出最少的步数。
这种算法的时间复杂为O(n^2),n为1字串的数目。因为这种渐近平方时间,所以Time Limit Exceed。
--------------------------------------------------------------------------------------------- */
#include<stdio.h>
#include<memory.h>
char szStr[100005] ;
int nOneGroup[55000] ; //记录连续1字串各有多少个1
int nZeroGroup[55000] ; //记录1字串之前0字串有多少个0
int main(void)
{
int i = 0 ;
int k = 0 ;
int j = 1 ;
int fFindOne = 0 ;
int nZeroIndex = -1 ;
int nOneIndex = 0 ;
int nMinStep = 0 ;
int nTempStep = 0 ;
int nTempZero = 0 ;
freopen("in.txt","r",stdin) ;
while(scanf("%s",szStr) != EOF)
{
i = k = 0 ;
nZeroIndex = -1 ;
nOneIndex = 0 ;
fFindOne = 0 ;
nMinStep = nTempStep = nTempZero = 0 ;
memset(nOneGroup,0,sizeof(nOneGroup)) ;
memset(nZeroGroup,0,sizeof(nZeroGroup)) ;
while(szStr[i] != '\0')
{
if('1' == szStr[i])
{
if(0 == fFindOne)
{
fFindOne = 1 ;
nZeroIndex++ ;
}
nOneGroup[nOneIndex]++ ;
}
else if(1 == fFindOne)
{
nZeroGroup[nZeroIndex]++ ;
if('1' == szStr[i+1])
{
fFindOne = 0 ;
nOneIndex++ ;
}
}
i++ ;
}
for(i = 0 ; i <= nOneIndex ; ++i)
{
nTempStep = 0 ;
nTempZero = 0 ;
for(k = i ; k > 0 ; --k) //左边的1字串向固定1字串移动
{
nTempZero += nZeroGroup[k-1] ;
nTempStep += nOneGroup[k-1]*nTempZero ;
}
nTempZero = 0 ;
for(k = i ; k < nOneIndex ; ++k) //右边的1字串向固定1字串移动
{
nTempZero += nZeroGroup[k] ;
nTempStep += nOneGroup[k+1]*nTempZero ;
}
if(0 == i)
{
nMinStep = nTempStep ;
}
else if(nTempStep < nMinStep)
{
nMinStep = nTempStep ;
}
}
printf("Case #%d: %d\n",j,nMinStep) ;
j++ ;
}
return 0 ;
}
/* -----------------------------------------------
在O(n^2)基础上,减去两个memset调用,降低一下常数因子...结果还是Time Limit Exceed
----------------------------------------------- */
#include<stdio.h>
char szStr[100005] ; //搞错了,原来是要10万个....
int nOneGroup[55000] ;
int nZeroGroup[55000] ;
int main(void)
{
long i = 0 ;
long k = 0 ;
long j = 1 ;
long fFindOne = 0 ;
long nZeroIndex = -1 ;
long nOneIndex = 0 ;
long nMinStep = 0 ;
long nTempStep = 0 ;
long nTempZero = 0 ;
while(scanf("%s",szStr) != EOF)
{
i = k = 0 ;
nZeroIndex = -1 ;
nOneIndex = 0 ;
fFindOne = 0 ;
nMinStep = nTempStep = nTempZero = 0 ;
while(szStr[i] != '\0')
{
if('1' == szStr[i])
{
if(0 == fFindOne)
{
fFindOne = 1 ;
nZeroIndex++ ;
}
nOneGroup[nOneIndex]++ ;
}
else if(1 == fFindOne)
{
nZeroGroup[nZeroIndex]++ ;
if('1' == szStr[i+1])
{
fFindOne = 0 ;
nOneIndex++ ;
}
}
i++ ;
}
for(i = 0 ; i <= nOneIndex ; ++i)
{
nTempStep = 0 ;
nTempZero = 0 ;
for(k = i ; k > 0 ; --k)
{
nTempZero += nZeroGroup[k-1] ;
nTempStep += nOneGroup[k-1]*nTempZero ;
if(i == nOneIndex)
{
nZeroGroup[k-1] = nOneGroup[k-1] = 0 ;
}
}
nTempZero = 0 ;
for(k = i ; k < nOneIndex ; ++k)
{
nTempZero += nZeroGroup[k] ;
nTempStep += nOneGroup[k+1]*nTempZero ;
}
if(0 == i)
{
nMinStep = nTempStep ;
}
else if(nTempStep < nMinStep)
{
nMinStep = nTempStep ;
}
if(i == nOneIndex)
{
nOneGroup[nOneIndex] = 0 ;
nZeroGroup[nOneIndex]= 0 ;
}
}
printf("Case #%d: %d\n",j,nMinStep) ;
j++ ;
}
return 0 ;
}
-------------------分割线-----------------------------
/* ------------------------------------------------
分析:
其实这种算法就是在平方算法的基础上做优化。
因为计算移向每一个固定中心1串的步数时,计算过程中所得到的部分结果也包含了计算移向下一个固定中心1
串的步数时所需要的数据,所以在计算当前步数时可以将这些结果储存起来,方便下一次计算,以做到线性
时间复杂度。
举个例子:
01串:011000111001111001
第一个固定中心1串是最左边的1串,其余所有1串向它靠近时,总共产生的步数有36步,也就是nAllToLeftStep
的值。
然后固定第二个1串时,相对于固定第一个1串时来说,右边的1串(包含第二个1串)向左移动的步数将会有所
下降,下降的数目就是第一个1串和第二个1串之前0的数目(nLeftZeroSum)乘以右边1串的数目(nRightOneSum),
也就是nDecreStep的值。这里解释一下,因为固定了第二个1串,所以原先本来要移向第1个1串的就只需要移动
到第二个1串则可,而第二个1串则不动,所以相对于nAllToLeftStep,固定第二个1串,移动少了nDecreStep步
数。因为固定中心向右移了,所以在固定中心左边的1串向他靠近的时候也会增加新的步数,也就是nIncreStep。
这里也需要保存下一次计算1串向右移的数据,也就是左边1串的数目(nLeftOneSum)乘以固定中心和前一个固定
中心之前0的数目(nZeroGroup[i-1])。
所以,总的来说就是线性时间,时间复杂度 O(n)。
------------------------------------------------ */
#include<stdio.h>
#include<memory.h>
char szStr[100005] ;
int nOneGroup[55000] ;
int nZeroGroup[55000] ;
int main(void)
{
long i = 0 ;
long k = 0 ;
long j = 1 ;
long fFindOne = 0 ;
long nZeroIndex = -1 ;
long nOneIndex = 0 ;
long nMinStep = 0 ;
long nTempStep = 0 ;
long nTempZero = 0 ;
long fCal = 0 ;
long nAllToLeftStep = 0 ;
long nRightOneSum = 0 ;
long nLeftZeroSum = 0 ;
long nLeftOneSum = 0 ;
long nDecreStep = 0 ;
long nIncreStep = 0 ;
while(scanf("%s",szStr) != EOF)
{
i = k = 0 ;
nZeroIndex = -1 ;
nOneIndex = 0 ;
fFindOne = fCal = 0 ;
nMinStep = nTempStep = nTempZero = nAllToLeftStep = nRightOneSum = 0 ;
nLeftZeroSum = nLeftOneSum = nDecreStep = nIncreStep = 0 ;
memset(nOneGroup,0,sizeof(nOneGroup)) ;
memset(nZeroGroup,0,sizeof(nZeroGroup)) ;
while(szStr[i] != '\0')
{
if('1' == szStr[i])
{
if(0 == fFindOne)
{
fFindOne = 1 ;
nZeroIndex++ ;
}
if('0' == szStr[i+1] || '\0' == szStr[i+1])
{
fCal = 1 ;
}
nOneGroup[nOneIndex]++ ;
if(nOneIndex >= 1 && 1 == fCal)
{
nTempZero += nZeroGroup[nOneIndex-1] ;
nTempStep += nTempZero*nOneGroup[nOneIndex] ;
nRightOneSum += nOneGroup[nOneIndex] ;
}
fCal = 0 ;
}
else if(1 == fFindOne)
{
nZeroGroup[nZeroIndex]++ ;
if('1' == szStr[i+1])
{
fFindOne = 0 ;
nOneIndex++ ;
}
}
i++ ;
}
nMinStep = nTempStep ;
nAllToLeftStep = nTempStep ;
for(i = 1 ; i <= nOneIndex ; ++i)
{
nTempStep = 0 ;
nLeftZeroSum = nZeroGroup[i-1] ;
nDecreStep += nRightOneSum * nLeftZeroSum ;
nLeftOneSum += nOneGroup[i-1] ;
nIncreStep += nLeftOneSum * nZeroGroup[i-1] ;
nTempStep = nAllToLeftStep - nDecreStep + nIncreStep ;
nRightOneSum -= nOneGroup[i] ;
if(nTempStep < nMinStep)
{
nMinStep = nTempStep ;
}
}
printf("Case #%d: %d\n",j,nMinStep) ;
j++ ;
}
return 0 ;
}
/* --------------------------------------------------------------------------------
少了两次memset调用,降低了常数因子,12MS
-------------------------------------------------------------------------------- */
#include<stdio.h>
char szStr[100005] ;
int nOneGroup[55000] ;
int nZeroGroup[55000] ;
int main(void)
{
long i = 0 ;
long k = 0 ;
long j = 1 ;
long fFindOne = 0 ;
long nZeroIndex = -1 ;
long nOneIndex = 0 ;
long nMinStep = 0 ;
long nTempStep = 0 ;
long nTempZero = 0 ;
long fCal = 0 ;
long nAllToLeftStep = 0 ;
long nRightOneSum = 0 ;
long nLeftZeroSum = 0 ;
long nLeftOneSum = 0 ;
long nDecreStep = 0 ;
long nIncreStep = 0 ;
while(scanf("%s",szStr) != EOF)
{
i = k = 0 ;
nZeroIndex = -1 ;
nOneIndex = 0 ;
fFindOne = fCal = 0 ;
nMinStep = nTempStep = nTempZero = nAllToLeftStep = nRightOneSum = 0 ;
nLeftZeroSum = nLeftOneSum = nDecreStep = nIncreStep = 0 ;
while(szStr[i] != '\0')
{
if('1' == szStr[i])
{
if(0 == fFindOne)
{
fFindOne = 1 ;
nZeroIndex++ ;
}
if('0' == szStr[i+1] || '\0' == szStr[i+1])
{
fCal = 1 ;
}
nOneGroup[nOneIndex]++ ;
if(nOneIndex >= 1 && 1 == fCal)
{
nTempZero += nZeroGroup[nOneIndex-1] ;
nTempStep += nTempZero*nOneGroup[nOneIndex] ;
nRightOneSum += nOneGroup[nOneIndex] ;
}
fCal = 0 ;
}
else if(1 == fFindOne)
{
nZeroGroup[nZeroIndex]++ ;
if('1' == szStr[i+1])
{
fFindOne = 0 ;
nOneIndex++ ;
}
}
i++ ;
}
nMinStep = nTempStep ;
nAllToLeftStep = nTempStep ;
for(i = 1 ; i <= nOneIndex ; ++i)
{
nTempStep = 0 ;
nLeftZeroSum = nZeroGroup[i-1] ;
nDecreStep += nRightOneSum * nLeftZeroSum ;
nLeftOneSum += nOneGroup[i-1] ;
nIncreStep += nLeftOneSum * nZeroGroup[i-1] ;
nTempStep = nAllToLeftStep - nDecreStep + nIncreStep ;
nRightOneSum -= nOneGroup[i] ;
if(nTempStep < nMinStep)
{
nMinStep = nTempStep ;
}
nZeroGroup[i-1] = nOneGroup[i-1] = 0 ;
if(nOneIndex == i)
{
nOneGroup[i] = 0 ;
nZeroGroup[i-1] = nZeroGroup[i] = 0 ;
}
}
if(0 == nOneIndex)
{
nZeroGroup[0] = nOneGroup[0] = 0 ;
}
printf("Case #%d: %d\n",j,nMinStep) ;
j++ ;
}
return 0 ;
}
-------------------分割线-----------------------------
/* ------------------------------------------------
最后就是想问一下一开始想到的动态规划法。
不知道对于这一道题来说,动态规划法是否可行?动态规划的可行条件为:最优子结构和无后效性,这两个
我都没有找到但是又觉得动态规划可行。因为上一种回溯法中已经涉及到动态决策问题,而动态规划可以用
在这些地方,这里的向左向右合并就涉及到了决策的问题。
动态规划法的核心是状态和状态转移方程,对于这一道题来说,状态应该是此时左边字串连续1的个数,而状
太转移方程应该就是向左向右移动产生的代价等等的方程(好吧,我不会写 = =)...
所以求指教.................
------------------------------------------------
-------------------分割线-----------------------------
附icelights同学的解法,一种完全不同的思路,很不错:http://blog.csdn.net/icelights/article/details/7713058
---------------------------------------------------