ACM-Position Arrangement (解题报告)

Time Limit:1000MS  Memory Limit:32768K

Description:

一个01串,我们可以对相邻两个字符实行交换位置的操作. 求最少的操作次数使得所有的1的位置连续. eg. s="010110",swap(s[1],s[2])之后,变成"001110". 所以答案是1.

Input:

多组数据,每组数据一个01字符串.串长不超过10^5

Output:

首先输出Case #k: ,然后是答案.

Sample Input:

010110
10101100
000

Sample Output:

Case #1: 1
Case #2: 3
Case #3: 0

Source:

dd

Status   Submit

 

 

题目链接: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

---------------------------------------------------

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值