1. 程式人生 > >( 題解 )第六屆藍橋杯決賽試題 -- 完美正方形 (線段樹 + 深搜)

( 題解 )第六屆藍橋杯決賽試題 -- 完美正方形 (線段樹 + 深搜)

題目 :

完美正方形

如果一些邊長互不相同的正方形,可以恰好拼出一個更大的正方形,則稱其為完美正方形。
歷史上,人們花了很久才找到了若干完美正方形。比如:如下邊長的22個正方形
2 3 4 6 7 8 12 13 14 15 16 17 18 21 22 23 24 26 27 28 50 60
如【圖1.png】那樣組合,就是一種解法。此時,
緊貼上邊沿的是:60 50
緊貼下邊沿的是:26 28 17 21 18
22階完美正方形一共有8種。下面的組合是另一種:
2 5 9 11 16 17 19 21 22 24 26 30 31 33 35 36 41 46 47 50 52 61
如果告訴你該方案緊貼著上邊沿的是從左到右依次為:47 46 61,
你能計算出緊貼著下邊沿的是哪幾個正方形嗎?

請提交緊貼著下邊沿的正方形的邊長,從左到右,用空格分開。


===================== 華麗的分割線 ===============================

當時在考場上看見這道題目的時候, 我果斷選擇了跳過, 首先當時就沒太多思路, 其次吧, 好不容易有了思路, 覺得貌似需要線段樹加深搜暴力破解, 想想肯定要耗不少時間來寫程式碼, 對於一道填空題而言, 不太划算啊, 所有就放棄了. 直到最近, 突然回想起此題, 就花了點時間做了一下( 主要是因為在度娘上沒有找到別人發的題解,所以想自己拿個沙發什麼的... ).

至於思路, 我們可以效仿[俄羅斯方塊]的思想, 在玩俄羅斯方塊的時候, 我們總會下意識地把方塊放置到高度最低的層上,同理,對於完美正方形的搭建, 由於題目已經規定了上邊緣的三個塊, 所以我們由上往下搭建, 而且我們知道邊長為154, 由此,可以定義 : 

1 .完美正方形的上邊沿表示為區間[L,R]=[0,153], 區間長度為R-L+1=154 .

2 .當我們沒有往裡面放置任何小正方形時, 區間[0,153]的高度為0, 且整體平滑.

3 .如果區間[L, R]不平滑, 則其高度定義為-1, 表示無意義.

4 .如果區間[L, R]平滑, 則其子區間[P, Q]平滑( 其中L<=P<=Q<=R)且高度相等.

5 .如果區間[L, R]不平滑,則其父區間[P, Q]不平滑( 其中P<=L<=R<=Q).

6 .每次只能往高度最小的平滑區間內從左向右地放置小正方形M, 但M的邊長不能大於區間長度.

大家看明白我的定義了嗎 ? 雖然有點難理解, 但我們可以舉個例子, 區間[0,153]的初始化高度為0, 整體平滑, 我們向裡面放置一個邊長為47的小正方形後, 區間[0,153]開始變得不平滑, 則置其高度為-1, 但其子區間[0,46]平滑且高度為47, 區間[47,153]平滑且高度依然為0. 我們繼續放置一個邊長為46和61的正方形後, 區間[0,153]則被分成了高度不同的三個平滑段, 其中高度最低的46.

有了上述定義, 那我們的目標就是, 每次找出高度最低的平滑區間, 嘗試在上面放置正方形, 直到區間[0,153]重新平滑且高度為154時, 結果成立.

為此, 我們需要以下的一些全域性變數:

#include <iostream>
using namespace std;

// 全域性變數 
namespace Global{ 
	const int MaxS = 46+47+61;	// 正方形的邊長 
	int All[] = {2,5,9,11,16,17,19,21,22,
			24,26,30,31,33,35,36,41,50,52};// 備選正方形邊長 
	int Length = sizeof(All)/sizeof(All[0]);// 備選正方形的數量 
	int Square[MaxS][MaxS]={0};	// 表示結果的完美正方形 
}


接下來, 是對線段樹的定義以及基本操作的方法定義.
// 線段樹結點
struct TNode{
	int L, R;			// [L, R]
	int height;			// 段高度( -1時表示該段不平滑,高度無意義 )
	int inc;			// 高度增量
	// 計算寬度 
	inline int Width()const{ return R-L+1;}
	// 計算中點
	inline int Mid()const{ return (L+R)>>1; } 
}NSet[1024];

inline int LSon( int i ){ return i<<1;}
inline int RSon( int i ){ return LSon(i)+1; }
inline int Parent( int i){ return i>>1;}

// 建立線段樹 
void BuildTree( int i, int L, int R )
{
	TNode* p = NSet+i;
	p->L = L;
	p->R = R;
	p->height = 0;	
	p->inc = 0;
	
	if( L < R ){
		int m = p->Mid();
		BuildTree( LSon(i), L, m );
		BuildTree( RSon(i), m+1, R);
	}
}
主要的問題在於, 如何對指定區間的高度進行增減, 以及在增減後維護其區間平滑性:
// 增量的向下傳遞調整 
// 注: NSet[i]必須為平滑區間 
inline void Adjust( int i )
{
	if( NSet[i].inc != 0 ){
		NSet[i].height += NSet[i].inc;
		if( NSet[i].L != NSet[i].R )
		{
			NSet[LSon(i)].inc += NSet[i].inc;
			NSet[RSon(i)].inc += NSet[i].inc;
		}
		NSet[i].inc = 0;
	}
}

// 將區間[L,R]的高度統一增加(或減少)inc
// 注: 區間 [L, R] 必須平滑 
void Add(int i, int L, int R, int inc )
{
	TNode* p = NSet+i;
	if( p->L == L && p->R == R ){
	  // 操作後平滑性不變 
		p->inc += inc;
		Adjust(i);
	}
	else{
		int m = p->Mid();
		
		if( p->height != -1 ) // 表示該段本是平滑的 
		{
			Adjust(i);
			p->height = -1;	  // 現在開始該段不再平滑 
		}
		
		int ls = LSon(i), rs = RSon(i);
		if( R <= m )
			Add( ls, L, R, inc );
		else if( L > m )
			Add( rs, L, R, inc );
		else{
			Add( ls, L, m, inc );
			Add( rs, m+1, R, inc );
		}
		
		// 平滑性恢復檢驗
		if(NSet[ls].height != -1){
			Adjust(ls);
			if(NSet[rs].height != -1){
				Adjust(rs);
				if(NSet[ls].height == NSet[rs].height)
					p->height = NSet[ls].height;
			}
		}		
	} // end else
}
有了上述的線段樹定義, 我們接下來需要定義一個能找出最小高度平滑區間的函式:
// 獲取最低段區間 
void GetLowestInterval(int i, TNode* prev, TNode* lowest )
{
	TNode* p = NSet+i;
	if( p->height == -1 )
	{
		GetLowestInterval( LSon(i), prev, lowest );
		GetLowestInterval( RSon(i), prev, lowest );
	}
	else
	{
		prev->R = p->R;
		// 檢驗等高區間連續性 
		if( p->height != prev->height )
		{
			prev->L = p->L;	
			prev->height = p->height;
		}
		
		// 更新最低區間
		if( prev->height <= lowest->height )
		{
			lowest->height = prev->height;
			lowest->L = prev->L;
			lowest->R = prev->R;
		}
	}
}
當這些基本的操作都實現後, 我們就可以專注暴力破解了:
bool Dfs( int L, int R ,int H )
{	
	if( L > R )
	{
		TNode p, m;
		m.height = 1000;
		m.L = m.R = 1000;
		p.height = 1000;
		p.L = p.R = -1;
		
		GetLowestInterval(1, &p, &m);
		L = m.L;
		R = m.R;
		H = m.height;
	}
	
	int Width = R-L+1, temp=0;
	if( Width == Global::MaxS && H != 0 ) return true;
	
	for( int i = Global::Length; i-- > 0 ;)
	{
		temp = Global::All[i];
		if( temp != 0 && temp <= Width )
		{
			Global::All[i] = 0;
			Add( 1, L, L+temp-1, temp);
			Global::Square[ H+temp-1 ][ L ] = temp;
			if( Dfs( L+temp, R, H ) == true ) return true;
			Add( 1, L, L+temp-1, -temp);
			Global::All[i] = temp;
		}
	}
	return false;
}
最後就是主函式啦
int main(int argc, char** argv) {
	BuildTree( 1, 0, Global::MaxS-1 );
		
	Add( 1, 0, 46, 47);
	Add( 1, 47, 92, 46);
	Add( 1, 93, 153, 61);

	if( Dfs( 1, 0, 0 ) ) {
		for( int i = 0,t=0; i < Global::MaxS; i+=t){
			t = Global::Square[ Global::MaxS-1 ][i];
			cout << t << ' ';
		}
	}
	return 0;
}
將近200行的程式碼到此結束了, 我還是覺得, 這種東西真的很難在考場上寫出了,量大,細節繁,分又不高, 也行是因為我技術有限吧, 如果有誰有更簡便的方法, 也歡迎評論吐槽~~




相關推薦

( 題解 )藍橋決賽試題 -- 完美正方形 (線段 + )

題目 : 完美正方形 如果一些邊長互不相同的正方形,可以恰好拼出一個更大的正方形,則稱其為完美正方形。 歷史上,人們花了很久才找到了若干完美正方形。比如:如下邊長的22個正方形 2 3 4 6 7 8 12 13 14 15 16 17 18 21 22 23 24 26

藍橋決賽試題: 對局匹配

題目: 標題:對局匹配 小明喜歡在一個圍棋網站上找別人線上對弈。這個網站上所有註冊使用者都有一個積分,代表他的圍棋水平。 小明發現網站的自動對局系統在匹配對手時,只會將積分差恰好是K的兩名使用者匹

【轉】藍橋決賽 第二題 完美正方形線段

完美正方形 如果一些邊長互不相同的正方形,可以恰好拼出一個更大的正方形,則稱其為完美正方形。 歷史上,人們花了很久才找到了若干完美正方形。比如:如下邊長的22個正方形 2 3 4 6 7 8 12 13 14 15 16 17 18 21 22 23 24 26 27 2

2015年藍橋JavaB組決賽題解——穿越雷區

標題:穿越雷區 X星的坦克戰車很奇怪,它必須交替地穿越正能量輻射區和負能量輻射區才能保持正常運轉,否則將報廢。 某坦克需要從A區到B區去(A,B區本身是安全區,沒有正能量或負能量特徵),怎樣走才能路徑最短? 已知的地圖是一個方陣,上面用字母標出了A,B區,其它區都標了正號或

2015藍橋國賽決賽c/c++本科B組試題總結及解題答案

1. 積分之迷 小明開了個網上商店,賣風鈴。共有3個品牌:A,B,C。 為了促銷,每件商品都會返固定的積分。 小明開業第一天收到了三筆訂單:  第一筆:3個A + 7個B + 1個C,共返積分:315  第二筆:4個A + 10個B + 1個C,共返積分:420 

算法筆記_208:藍橋軟件類決賽真題(Java語言A組)

boolean style 空格 ima eight jdk1 ++ port 但是 目錄 1 胡同門牌號 2 四階幻方 3 顯示二叉樹 4 穿越雷區 5 切開字符串 6 鋪瓷磚 前言:以下代碼僅供參考,若有錯誤歡迎指正哦~ 1 胡同門牌號 標題:胡

2015年藍橋C/C++程序設計本科B組決賽 ——居民集會(編程大題)

con 要求 數據 color 資源 例如 aps 計算 調試 標題:居民集會 藍橋村的居民都生活在一條公路的邊上,公路的長度為L,每戶家庭的 位置都用這戶家庭到公路的起點的距離來計算,第i戶家庭距起點的距 離為di。每年,藍橋村都要舉行一次集會。今年,由於村裏的人口太多,

藍橋題解

1、獎券數目 法一: #include<stdio.h> int main() { int ans=0; for(int i=1; i<=9; i++) for(int j=0; j<=9; j++)

2015年藍橋試題(C/C++本科B組)

1 有些人很迷信數字,比如帶“4”的數字,認為和“死”諧音,就覺得不吉利。雖然這些說法純屬無稽之談,但有時還要迎合大眾的需求。某抽獎活動的獎券號碼是5位數(10-99),要求其中不要出現帶“4”的號碼,主辦單位請你計算一下,如果任何兩張獎券不重號,最多可發出獎券多少張。 請

藍橋大賽個人賽決賽(軟體類)真題 Java語言B組 答案

標題:分機號X老闆脾氣古怪,他們公司的電話分機號都是3位數,老闆規定,所有號碼必須是降序排列,且不能有重複的數位。比如:751,520,321 都滿足要求,而,766,918,201 就不符合要求。現在請你計算一下,按照這樣的規定,一共有多少個可用的3位分機號碼?請直接提交該

2015年藍橋本科B組C++省賽個人題解

比賽結束已經一星期了,成績也出來了,江蘇非211組的省前十,但是深感自己還是有太多的不足。絕對不能以自己還只是大一為藉口,acm這條路還長的很。 目測得了95分(滿分150),第一題錯了,程式碼填空第一題錯了,倒數第二題扣了一點分,最後一道大題全錯。 之所以會這麼

2015年藍橋C/C++程式設計本科B組決賽 密文搜尋(程式設計大題)

2015年第六屆藍橋杯C/C++程式設計本科B組決賽題目彙總: 密文搜尋 福爾摩斯從X星收到一份資料,全部是小寫字母組成。 他的助手提供了另一份資料:許多長度為8的密碼列表。 福爾摩斯發現,這些

穿越雷區藍橋大賽個人賽決賽(C語言A組)四題

標題:穿越雷區 X星的坦克戰車很奇怪,它必須交替地穿越正能量輻射區和負能量輻射區才能保持正常運轉,否則將報廢。 某坦克需要從A區到B區去(A,B區本身是安全區,沒有正能量或負能量特徵),怎樣走才能路徑最短? 已知的地圖是一個方陣,上面用字母標出了A,B區,其它區都標了正號

藍橋省賽試題--壘骰子 解題報告

PS: 關於本題演算法的優化演算法已經發表, 請檢視疊骰子( 以矩陣方法實現 ) 原題: 賭聖atm晚年迷戀上了壘骰子,就是把骰子一個壘在另一個上邊,不能歪歪扭扭,要壘成方柱體。 經過長期觀察,atm 發現了穩定骰子的奧祕:有些數字的面貼著會互相排斥! 我們先來規範一下骰子

藍橋B組C++試題

4.  格子中輸出 StringInGrid函式會在一個指定大小的格子中列印指定的字串。 要求字串在水平、垂直兩個方向上都居中。 如果字串太長,就截斷。 如果不能恰好居中,可以稍稍偏左或者偏上一點。 下面的程式實現這個邏輯,請填寫劃線部分缺少的程式碼。 #include <stdio.h> #in

藍橋省賽試題--壘骰子 以矩陣的方法實現 解題報告

本貼宣告: 關於這道題的基本解法, 我在之前曾經發表過, 以動態規劃的方式在O(N)的時間複雜度內求解, 但對於資料規模為10^9的資料而已, O(N)顯然是不夠的, 當時我受困良久. 但幸運的是, 某網友給了我一個萬分有用的建議, 以矩陣的方式的進行求解. 當我實現以後,

藍橋【省賽試題9】壘骰子 ( 矩陣快速冪 )

題目描述: 賭聖atm晚年迷戀上了壘骰子,就是把骰子一個壘在另一個上邊,不能歪歪扭扭,要壘成方柱體。  經過長期觀察,atm 發現了穩定骰子的奧祕:有些數字的面貼著會互相排斥! 我們先來規範一下骰子:

藍橋 穿越雷區 2015年藍橋JavaB組決賽四題

標題:穿越雷區 X星的坦克戰車很奇怪,它必須交替地穿越正能量輻射區和負能量輻射區才能保持正常運轉,否則將報廢。 某坦克需要從A區到B區去(A,B區本身是安全區,沒有正能量或負能量特徵),怎樣走才能路徑最短? 已知的地圖是一個方陣,上面用字母標出了A,B區,其它區都標了正號

藍橋大賽試題題解

第一題標題:貪吃蛇長度+-------------------------------------------------+|                                                 ||    H######             

藍橋【省賽試題3】三羊獻瑞

題目描述: 觀察下面的加法算式:   其中,相同的漢字代表相同的數字,不同的漢字代表不同的數字。  請你填寫“三羊獻瑞”所代表的4位數字(答案唯一),不要填寫任何多餘內容。 題目答案: 1085