SRM667 DIV2 題解

這是掉到DIV2後的第二次比賽,本以爲題目簡單,但沒想到又掉坑裏。。。

第一題,在一個100 * 100的二維平面內找一個滿足一個條件的點,無任何亮點,枚舉即可。

第二題,初始情況下有一個空Cache,假設系統共有N個元素,用位表示法來表示,0表示第i個元素不存在,1表示存在,所以初始Cache就是是000... 000。

接下來有M個操作,每次操作都會讀取一些元素(例如,011表示用到了第2,3元素),如果數據在Cache中則消耗時間爲0,如果不在Cache中,系統要將元素Cache進來,消耗的時間是[需取元素個數] ^ 2。問如何排列這M個操作消耗總時間最少,輸出最少時間。N, M <= 20。

分析:如果消耗時間是不是平方關係,只是線性關係,那麼最後消耗時間就是最後Cache包含的元素個數,無所謂操作的序列了。但是,有平方關係就不一樣了,由於每一次需要取的新元素個數是不一樣的,平方之後會對最後結果產生影響。比如:有3個操作,分別要取1000, 1100, 0111:
  • 方案一:1000 -> 1100 ->0111,結果就是1 ^ 2 + 1 ^ 2 + 2 ^ 2 = 6,
  • 方案二:0111-> 1000 -> 1100,結果就是3 ^ 2 + 1 ^ 2 = 10。

顯然不同操作順序會影響最後的總消耗時間,題目的意思實際上就是拆成M項數字,讓這M項的平方和最小。

通過以上分析,有些讀者可能會像到貪心的算法,就是每次儘量的少取新元素,這樣分解的結果會是每一項都非常小,詐看是有道理的,但是犯了和筆者一樣的問題,由於這裏的操作並不能形成一個鏈,有許多分叉,操作之間有交叉關係,貪心眼前最優值並不能導致全局最優,比如序列:0011,1100,1110,11111。如果按貪心的來就是0011->1100->1110->1111,這樣的結果是8,但是正確6。


既然貪心不對,那麼怎麼做呢,觀察到N <= 20,可以暴力枚舉每一種Cache狀態的最少消耗時間,一共有2 ^ 20種Cache狀態,DP[state] = DP[last_state] + newEle ^ 2。 last_state可以枚舉當前的操作,所以時間複雜度是O(20 * 2 ^ 20)。

class OrderOfOperationsDiv2 {
	public:
	
	int getOneNum(int num) {
		int res = 0;
		while(num) {
			num &= (num - 1);
			res++;
		}

		return res;
	}

	int getVal(string s) {
		int res = 0;
		for(int i = s.size() - 1;i >= 0;i--) {
			res <<= 1;
			if (s[i] == '1')
				res++;
		}

		return res;
	}

	int minTime(vector <string> s) {
		int dp[1 << 20];
		memset(dp, -1, sizeof(dp));
		dp[0] = 0;
		
		int res = 0;
		for(int i = 0;i < 1 << 20;i++) {
			if (dp[i] == -1)
				continue;

			for(int j = 0;j < s.size();j++) {
				int sInt = getVal(s[j]);
				int nextInt = sInt | i;
				int oneNum = getOneNum(nextInt ^ i);
				
				if (dp[nextInt] == -1)
					dp[nextInt] = dp[i] + oneNum * oneNum;


				// cout << nextInt << " " << dp[nextInt] << endl;
				dp[nextInt] = min(dp[nextInt], dp[i] + oneNum * oneNum);
				res = dp[nextInt];
			}
		}
		
		return res;
	}
};


第三題,題目背景是一個如何最優地建造商店問題,N個地基排成一行,每個地基可以蓋多層(意味着可以有多個商店),但是這個地基上的商店收益與左右地基中商店個數有關profit[x][y],x是該位置上的商店數,y是左右位置上的商店總數,最多可以建造M個商店。問如何建造商店能得到最大收益。M, N <= 31。

想一下最原始的順序DP是怎麼樣的,從左向右順序遞推,dp[i][t] = dp[i - 1][s] + profit,但是這裏除了左邊,還要考慮右邊。如果考慮其他方案可能比較都比較麻煩,這裏有一種特別好的方法,就是拓展狀態。既然右邊需要考慮,那麼就右邊的建造數加到狀態中,這樣就能唯一狀態了。所以,狀態改成dp[i][t][r],表示第i位上建造t個商店,並且右邊有r個商店,前i個位置取得的最好的利益值。所以遞推方程就是:

dp[i][t][r] = dp[i – 1][l][t] + profit[l][r] { 0 =< t <= m, 0 =< l <= m }

最終結果是max(dp[N – 1][t][0]), 時間複雜度是O(N * m ^ 3)。


class ShopPositions {
	public:
	int maxProfit(int n, int m, vector <int> c) {
		int dp[31][31][31];
		memset(dp, 0, sizeof(dp));

		for(int i = 0;i < n;i++) {
			// 0 taco
			if (i > 0) {
				int maxVal = 0;
				for(int j = 0;j <= m;j++) {
					maxVal = max(maxVal, dp[i - 1][j][0]);
				}
				for(int j = 0;j <= m;j++) {
					dp[i][0][j] = maxVal;
				}
			}

			for(int j = 1;j <= m;j++) {
				for(int l = 0;l <= m;l++) {
					for(int r = 0;r <= m;r++) {
						if (i == 0)
							dp[i][j][r] = c[i * 3 * m + j + r- 1] * j;
						else
							dp[i][j][r] = max(dp[i][j][r], dp[i - 1][l][j] + c[i * 3 * m + j + l + r - 1] * j);
					}
				}
			}
		}

		int res = 0;
		for(int i = 0;i <= m;i++) {
			res = max(res, dp[n - 1][i][0]);
		}

		return res;
	}

};

作爲一個程序設計者,最重要的就是分析能力,不管是算法,還是其他領域,對任何問題都要有剖開問題,重新組織的能力。這次比賽的後兩道題都比較不錯,第二題很容易想成貪心,而第三題又能訓練對動態規劃的狀態的理解。

有一些教訓,就是當不確定算法是否正確的時候,可以通過測試驅動的方式來進行編程,多想測試用例能夠去除掉具有顯著錯誤的算法,這一點尤其在短時間內是有用的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章