1. 程式人生 > >CodeForces 372B 腦洞大開的DP遞推

CodeForces 372B 腦洞大開的DP遞推

題目

做了兩個多小時,腦洞大開,給了一個01矩陣,求以a,b,為左上角,c,d為右下角的矩陣內有多少包含部分全為0的子矩陣

對於這道題目,一開始就想到了DP遞推,感覺而已,雖然準,可是做不出啊,想好了遞推式子可是細節部分沒辦法去處理。看了CF上的題解,頓時腦洞大開,這個做法真的是太厲害了,這方法程式碼簡潔明瞭,同時也提醒到了我,在方程假設出來後,對於轉移的細節處理,

其實一開始我想到過這個遞推式子

dp[i][j][k][l] += dp[i][j][k - 1][l] + dp[i][j][k][l - 1] - dp[i][j][k - 1][l - 1];對於這個式子,就還差一個,那就是包含(c,d)也就是右下角的矩陣數目,這個當時無法去求,現在覺得這方法真的太巧妙了,可以先開一個二維陣列last[i][j],代表當前(i,j)這個點距離它同一行左邊距離它最近的 一個1的距離,如果它左邊沒有1,那麼就是距離左邊第一列 也就是矩陣左邊界的距離,

一開始設定一個值 l - j + 1,也就是左上角的點與右下角的點的 列數之間的距離,然後開始列舉從右下角到左上角的 行數 中的last[kk][l]的值,(i<=kk<=k),取兩個之間曉得的那個加上,為什麼呢

比如說:

000

000,這個子矩陣一開始l - j + 1 = 3,然後列舉行數,發現最終答案是6,數一數包含右下角的點的 矩陣個數確實是六,因為要包含右下角所以多一行其實就是個數多了一倍而已

再舉個例子:

010

000,一開始l - j + 1 = 3,開始列舉行數,最終答案是4,為什麼呢,因為要包含右下角的點,所以當你一旦遇到1的時候,那麼其實從這個1所在位置開始的 左邊上邊部分都可以去掉了,只有這個1以及1上邊的右邊部分可以用了, 這方法真神!

int n,m,q;

int mp[40 + 5][40 + 5];

int dp[40 + 5][40 + 5][40 + 5][40 + 5];

int last[40 + 5][40 + 5];//與左邊最近的一個1的距離,

void init() {
	memset(dp,0,sizeof(dp));
	memset(last,0,sizeof(last));
}

bool input() {
	while(cin>>n>>m>>q) {
		for(int i=1;i<=n;i++) {
			string s;
			cin>>s;
			for(int j=1;j<=m;j++) {
				mp[i][j] = s[j - 1] - '0';
			}
		}
		return false;
	}
	return true;
}

void slove() {
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=m;j++) {
			if(mp[i][j]){last[i][j] = 0;continue;}
			last[i][j] = last[i][j - 1] + (mp[i][j] == 0);
		}
	}
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=m;j++) {
			for(int k=i;k<=n;k++) {
				for(int l=j;l<=m;l++) {
					int tmp = l - j + 1;
					//int tt = 0;
					for(int kk = k;kk >= i;kk--) {
						tmp = min(tmp,last[kk][l]);
						dp[i][j][k][l] += tmp;
						//tt += tmp;
						//cout<<tt<<"****"<<endl;
					}
					dp[i][j][k][l] += dp[i][j][k - 1][l] + dp[i][j][k][l - 1] - dp[i][j][k - 1][l - 1];
				}
			}
		}
	}
}

void cal() {
	slove();
	while(q--) {
		int aa,bb,cc,dd;
		cin>>aa>>bb>>cc>>dd;
		cout<<dp[aa][bb][cc][dd]<<endl;
	}
}

void output() {

}

int main() {
	while(true) {
		init();
		if(input())return 0;
		cal();
		output();
	}
	return 0;
}