1. 程式人生 > >二維字首和詳解

二維字首和詳解

這幾天在打比賽時遇到了二維字首和,看了一下深有體會,發一篇詳解。 首先,什麼是字首和?一個數列,我們要計算某個區間內的和,該怎麼做呢?正所謂暴力出奇跡,這一個也可以,我們暴力列舉每一個區間內的數並且相加,可是這個是O(n)的時間複雜度,不要小看這個線性,可如果在DP裡的話這相當於加了一次方,卡你非常簡單,下面講一下O(n)的預處理,O(1)的計算,你可能會說這個也是線性,有差嗎?當然有差,這個是需處理,不會像暴力一樣在DP中使DP的時間複雜度加一次方而是直接加上O(n)。下面講一下做法,字首和,顧名思義就是第幾個數之前的數的和,我們用DP來預處理,我們定義狀態DP[i]表示到第i個數(包括它)為止前面所有數的和,從而得出狀態轉移方程DP[i]=DP[i-1]+num[i],num[i]表示陣列中第i個數,這樣要計算閉區間i,j(閉區間就是指i<=x<=j,x就是區間裡的數)的和就是DP[j]-DP[i-1],這個畫圖理解如下:在這裡插入圖片描述

下面我們講一下什麼是二維字首和,建立在一維字首和之上,我們要求一個矩陣內一個任意的子矩陣的數的和,我們就可以用二維字首和,我們還是用DP來預處理,狀態和一維字首和差不多,只不過我們多加了一維,DP[i][j]表示(1,1)這個點與(i,j)這個點兩個點分別為左上角和右下角所組成的矩陣內的數的和,好好想一下裝太專移方程,DP[i][j]=DP[i-1][j]+DP[i][j-1]-DP[i-1][j-1]+map[i][j],怎麼來的呢?我們畫一下圖就知道了。 在這裡插入圖片描述 這張圖就知道了(i,j)可以由(i-1,j)和(i,j-1)兩塊構成,不過要注意兩個點,1、有一塊矩陣我們重複加了,也就是(i-1,j-1)這一塊,所以我們要減去它。2、我們這個矩陣是不完整的,由圖可知我們還有一塊深藍色的沒有加,也就是(i,j)這一塊,所以我們要再加上map[i][j]也就是題目給出的矩陣中這一格的數。這樣我們就預處理完了,現在講一下怎麼通過我們的預處理從而快速地得出我們想要的任意子矩陣中的和,我們定義(x1,y1)為我們想要子矩陣的左上角,(x2,y2)為我們想要子矩陣的右下角,然後我們畫圖想一想。 在這裡插入圖片描述
我們可以通過DP[x2][y2]來計算,我們通過圖可以發現這個距離我們要的還差紅色的部分看看怎麼表示紅色部分?我們可以分割成兩塊,分別是DP[x1][y2]和DP[x2][y1]我們發現有一塊重複減了,所以我們再加上它即DP[x1][y1],有一點注意,因為畫圖和定義原因我們發現邊界好像不對,我們來看看,我們定義的狀態是整個矩陣包括邊的和,而我們要求的也是要包括邊的,所以我們要再改一下,把DP[x1][y2]和DP[x2][y1]和DP[x1][y1]分別改成DP[x1-1][y2]和DP[x2-1][y1]和DP[x1-1][y1-1]這樣一減我們就可以得到自己想要的答案,整理可得公式,DP[x2][y2]-DP[x1-1][y2]-DP[x2-1][y1]+DP[x1][y1]這樣我們就可以做到O(1)之內查詢,很奇妙吧,我們看一下實現程式碼:

#include<iostream>
#include<cstring>
using namespace std;
int dp[2000][2000],map[2000][2000];
int main()
{
	int m,n,k;//所給的矩陣是n*m的,有k組查詢 
	cin >>n>>m>>k;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin >>map[i][j];
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++)//預處理一波 
		for(int j=1;j<=m;j++)
			dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1]+map[i][j];
	for(int i=1;i<=k;i++)//接受查詢 
	{
		int x1,x2,y1,y2;
		cin >>x1>>y1>>x2>>y2;
		cout <<(dp[x2][y2]+dp[x1-1][y1-1]-dp[x1-1][y2]-dp[x2][y1-1])<<endl;//O(1)查詢 
	}
	return 0;
} 

這樣二維字首和就講完了,下面給一個不錯的例題: 模板題 給一下題解:弱雞寫的題解 希望這一篇演算法解析對大家有幫助。