1. 程式人生 > >杭電 漢諾塔問題總結

杭電 漢諾塔問題總結

看了一下杭電的各種漢諾塔問題,遇到些奇奇葩葩的小問題,也有很多很好的思想,比如最後一題,來來回回的顛倒很有意思。總結一下;

意思是給把原始的漢諾塔問題中的3根柱子改為4根。做了半天各種WA。查了一下,有一篇文章詳細講了一下,還做出了遞迴公式以及數學公式:

程式碼如下:

#include<cstdio>
#include<cmath>
#include<algorithm>
//F(n)=min(2*F(n-r)+2^r-1),(1≤r≤n)
using namespace std;
int main()
{
    int n,m;
    long long Min,f[65];
    f[1]=1;
    f[2]=3;
    for(int i=3;i<=65;i++)
    {
        Min=99999999;
        for(int j=1;j<i;j++)
            Min=min(2*f[j]+pow(2.0,i-j)-1,Min*1.0);
        f[i]=Min;
    }
    while(~scanf("%d",&n))
        printf("%lld\n",f[n]);
    return 0;
}

按照公式寫就是了,寫的時候需要注意一下精度問題。2^r可以寫為1<<r,不過因為數字常數1預設是32位,所以如果要使用位移的話,一定要先宣告一個longlong型別的變數來進行位移,否則 就會出現溢位錯誤,這個我糾結了一陣子,感覺沒錯,一提交就WA,然後試了試64發現果然是負數。

哎,基礎還是不紮實啊。

用pow函式因為返回的是一個double型別,min函式裡比較也是用double來做的,只是在最後賦值的時候取int型就可以,所以不會出錯。

Pro.ID  1995 漢諾塔V

這個是普通的漢諾塔,最優的步數是2^n-1,只不過問的第i個盤子移動的次數。依然是用遞迴,在紙上畫畫就能出來。

注意了,第i盤子,不用考慮底下的盤子,只用看之上的經過一個柱子到達目的地。即F[n]=2*f[n-1]

程式碼:

#include<iostream>

using namespace std;

int
main()
{
    __int64 s[61] = {0, 1};
    int n, i, t, m;

    for(i = 2; i < 61; i++)
        s[i] = s[i - 1] * 2;

    cin >> t;
    while(t--)
    {
        cin >> n >> m;

        cout << s[n - m + 1] << endl;
    }
}
Pro.ID 漢諾塔VI

是問所有步驟,注意不是最優的,是全部(當然不包括錯誤的步驟)

每一個盤子可以放到3根柱子的任意一個,所以是3^n。比如正確的是直接從a->c,現在可以a->b然後在b->c,就是多了2種。每一個都多了2種,所以是3^n。

程式碼:

#include<iostream>
#include<math.h>
#include<stdio.h>
using namespace std;
int main()
{
    __int64 t,n,i;
    __int64 sum,a[100]={3,};
    while(cin>>t)
    while(t--)
    {
        cin>>n;
        for(i=1;i<n;i++)
        a[i]=a[i-1]*3;
        cout<<a[n-1]<<endl;
    }
    return 0;
}
Pro.ID 1997 漢諾塔VII

題目是說,給定某一時刻的三個柱子上的盤子,問這個是不是符合最優解過程中某一時刻的狀態。

思想是:

對一個含有n個盤子,從A柱移動到C柱藉助B柱的漢諾塔,第n個盤子移動到C柱過程是這樣子的:首先將其餘的n-1個盤子移動到B柱,然後第n個盤子直接移動到C柱。在這過程中,第n個盤子只出現在A柱和C柱兩個柱子上,也即第n個盤子不可能出現在B柱上。因此對於當前移動的盤子,只需檢查其所在的柱子是否符合這個要求,如果出現在B柱上,則顯然進入了錯誤移動中。這是本題求解思想精髓所在。

程式碼就不貼了,上邊這個部落格裡寫的很詳細。

Pro.ID 2064 漢諾塔III

還是遞推:num[i]=3*num[i-1]+2;

不解釋了,程式碼如下:

#include<iostream> 
#include<cstdio>  
#include<cstring> 
#include<cmath>    
using namespace std;      
unsigned long long num[36];
int main()
{              
    num[1]=2;
    num[2]=8;
    for(int i=3;i<=35;i++)
        num[i]=3*num[i-1]+2;
    int n;
    while(~scanf("%d",&n))
        cout<<num[n]<<endl;
    return 0;
}

Pro.ID 2077 漢諾塔IV(參考了zz_zigzag的部落格)

好無聊啊,把上邊的規則給改了,只是最大的可以放上邊。其實感覺這個題目跟之前那個1027題目有點想通之處,在1027中說的是4根柱子,所以通用的這3個步驟其實並非最優解:

(1)1柱藉助3…M柱子將n-(M-2)個盤子移動到2柱上。

(2)M-2個通過3…M-1柱簡單的移動到M柱上【2*(M-2)-1步驟】。

(3)2柱藉助13…M-1柱子將n-(M-2)個盤子移動到M柱上。

如果有n個盤子,則需要前n-1個挪到中間的盤子上,然後最大的盤子挪到最右面,需要兩步,把前(n-1)個盤子從左邊挪到中間是和從中間挪到右邊需要相同的次數。而a陣列中存放的就是那個前n-1個盤子挪動到相同位置需要的次數。結果即為a[i-1]*2+2。

所以我直接想成了是f[n]=2*f[n-1]+2,結果錯了。【因為是需要n-1個盤子前進一步】


而求a陣列需要用到遞推。公式為第i個為前n-1個移動次數的三倍加一,簡化到兩個盤子,小的先移動兩次到最右邊,大的移到中間,然後小的在移回中間,小的移動了三次,而大的移動了一次,就使他們全部挪動了一個位置

所以程式碼如下:

#include<stdio.h>
int a[20]={0,1};
 
int main()
{
    int i,T;
    for(i=2;i<21;i++)
    {
        a[i]=3*a[i-1]+1; 
    }
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d",&i);
        printf("%d\n",2*a[i-1]+2);
    }
    return 0;
}

Pro.ID 2175 漢諾塔IX

普通漢諾塔,問在最優步驟中的第i步是哪一個盤子,跟1995那個題目剛好相反。不過這個有點像數論題。

這樣想,假設是4個盤子,考慮第三個,在第4步的時候將3盤從A移動到C【設目的地是B】,此時1,2盤在B上,設時間為T,然後將1,2盤移動到C上,(需要3步)再把4盤移動到B上,此時的格局為4盤在B上,1,2,3,在C上,距T過去了1+3=4步,那麼3號盤什麼時候再動呢?把1,2移走,3就可以放到B上了,移走1,2需要花費3個步驟,因此距T4+3+1也就是第8步,總體是第12步時,3號盤子會再次移動。現在看明白了吧,就是基數倍的2^(i-1)時,i號盤子會移動。

程式碼如下:

#include<iostream>
using namespace std;
int main()
{
    __int64 a[65];
    a[1]=1;
    __int64 i,n,r;
    __int64 m;
    for(i=2;i<=63;i++)
        a[i]=2*a[i-1];
    while(~scanf("%I64d%I64d",&n,&m),n+m)
    {
        for(i=1;i<=n;i++)
        {
            r=m/a[i];
            if(r%2==1&&m%a[i]==0)
                printf("%I64d\n",i);
        }
    }
    return 0;
}
Pro.ID 2184 漢諾塔VIII

感覺這個題目非常的好,挺有意思的,問普通漢諾塔,N個盤子,在最優解的第M步時,每個柱子上的盤子的狀態。

想了半天,也沒什麼思路,但有一點是絕對可以確定的,就是一般解簡單漢諾塔過程的問題都是使用遞迴,可以得到全部過程,但是當N稍微到10以上的時候必然遞迴很慢,所以直接遞迴模擬必然是錯誤的,但是根據上一題目,第i個步移動哪一個盤子中確定的,第K個盤子在  奇數*2^(K-1)時移動可以得到些思想,必然是根據步數來確定盤子。

但是想了老半天也不太清楚那個遞迴改怎麼寫,好像每一次判斷都要做除法到是可以確定某一個盤子,但是如何確定所有的盤子呢?糾結啊

查了半天,找到了一個大神的程式碼。

講的非常的詳細,我把思路以及程式碼粘過來大家分享一下:

/*

定義陣列a,其中a[i]表示完成一次i層漢諾塔移動的次數。
指標o,p,q分別表示三個位置。
起始狀態為n層都在o上,要往q方向移動。
然後分成兩種情況:
1、
m<=a[n-1];
此時,第n層沒機會移動,那麼就相當於o上的n-1層往p上移動。
使其狀態和起始狀態一致,我們要交換p和q。
2、
m>a[n-1];
此時,先進行到下面狀態,上面n-1層移動到p位置,第n層移動到q位置,消耗了a[n-1]+1次移動。
接下來就變成p上的n-1層往q上移動,只要交換o,p,令m=m-a[n-1]-1即可。

通過上述操作,都可以得到第n層的位置,並且問題變成n-1層都在o上,要往q方向移動。
*/
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
int main()
{
	unsigned __int64 m,a[64];
	int row[3][66];
	a[1]=1;
	a[0]=0;
	for(int i=2;i<=63;i++)
		a[i]=a[i-1]*2+1;
	int t;
	scanf("%d",&t);
	while(t--)
	{
		int n;
		scanf("%d %I64u",&n,&m);
		int *start,*mid,*end;
		start=row[0];
		mid=row[1];
		end=row[2];
		*start=*mid=*end=1;
		while(n)
		{
			n--;
			if(m<=a[n])
			{
				*(start+*start)=n+1;//從第二個位置開始記錄盤子
				(*start)++;//第一個位置表示的是這個柱子一共有多少個盤子
				swap(end,mid);
			}
			else
			{
				*(end+*end)=n+1;
				(*end)++;
				swap(start,mid);
				m-=(a[n]+1);
			}		
		}
		for(int i=0;i<3;i++)
		{
			printf("%d",row[i][0]-1);
			for(int j=1;j<row[i][0];j++)
				printf(" %d",row[i][j]);
			printf("\n");
		}
	}
	return 0;
}


Pro.ID 漢諾塔 X

進一步加強條件,在求第m步時是哪個盤子動,怎麼動。

必然遞迴啊。把上上個題目修改就可以了。具體的就不多說了,在註釋裡有詳細解釋

#include<iostream>
using namespace std;
 __int64 a[65];
void solve(int n,__int64 m,int start,int end)
{
	int third=6-start-end;//得到第3跟柱子
	__int64 mid=a[n];
	if(m==mid) //如果是當前盤子移動,直接從start-->end
	{
		printf("%d %d %d\n",n,start,end);
		return ;
	}
	if(m<mid)//當前盤子無法移動,必然是上邊的某個盤子動,並且移動一定是到third號柱子上,遞迴求解
		solve(n-1,m,start,third);
	else//需要先移動當期盤子下部的盤子(參考2184題目)
		solve(n-1,m-mid,third,end);
}
		
int main()
{
    __int64 m;
    a[1]=1;
	int n;
    for(int i=2;i<=63;i++)
        a[i]=2*a[i-1];
	int t;
	while(~scanf("%d",&t))
	{
		while(t--)
		{
			scanf("%d%I64d",&n,&m);
			solve(n,m,1,3);
		}
	
	}
	return 0;
}

最後一個Pro.ID  2587:很O_O的漢諾塔

真心是跪了,