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;
	}

};

作为一个程序设计者,最重要的就是分析能力,不管是算法,还是其他领域,对任何问题都要有剖开问题,重新组织的能力。这次比赛的后两道题都比较不错,第二题很容易想成贪心,而第三题又能训练对动态规划的状态的理解。

有一些教训,就是当不确定算法是否正确的时候,可以通过测试驱动的方式来进行编程,多想测试用例能够去除掉具有显著错误的算法,这一点尤其在短时间内是有用的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值