字首和與二維字首和
考慮一個序列 \(a\),我們如何快速求出區間 \([l, r]\) 的元素和呢?這很簡單,我們只需求出它的字首和序列 \(\mathrm{sum}(i) = \sum_{k = 1}^i a_i\),那麼答案即為 \(\mathrm{sum}(r) - \mathrm{sum}(l - 1)\)。而對於序列 \(\mathrm{sum}\),有 \(\mathrm{sum}(i) = \mathrm{sum}(i - 1) + a_i\),可以在 \(\Theta(n)\) 時間內遞推求出。因此藉助字首和,我們即可做到 \(\Theta(n)\) 預處理、\(\Theta(1)\) 查詢。
進一步地,考慮把這個問題擴充套件到二維。對於一個矩陣 \(a\),我們如何快速求出行座標為 \([l_1, r_1]\)、列座標為 \([l_2, r_2]\) 的子矩陣中所有元素的和呢?這也不難,我們記一個類似的字首和矩陣 \(\mathrm{sum}(i, j)\),表示行座標為 \([1, i]\)、列座標為 \([1, j]\) 的子矩陣中所有元素的和。很自然地,我們考慮容斥,那麼答案為 \(\mathrm{sum}(r_1, r_2) - \mathrm{sum}(l_1 - 1, r_2) - \mathrm{sum}(r_1, l_2 - 1) + \mathrm{sum}(l_1 - 1, l_2 - 1)\)。這就是說,藉助字首和矩陣 \(\mathrm{sum}(i, j)\),我們就可以使用容斥在 \(\Theta(1)\) 的時間複雜度內求出上述問題的答案。
問題轉化為如何求出字首和矩陣 \(\mathrm{sum}(i, j)\)。事實上,對於元素 \(a_{i, j}\),我們可以將其看作行座標為 \([i, i]\)、列座標為 \([j, j]\) 的子矩陣,根據答案的表示式有 \(a_{i, j} = \mathrm{sum}(i, j) - \mathrm{sum}(i - 1, j) - \mathrm{sum}(i, j - 1) + \mathrm{sum}(i - 1, j - 1)\),因此我們也可以使用類似的容斥得出 \(\mathrm{sum}(i, j) = a_{i, j} + \mathrm{sum}(i - 1, j) + \mathrm{sum}(i, j - 1) - \mathrm{sum}(i - 1, j - 1)\)。即對於矩陣 \(\mathrm{sum}(i, j)\),我們也可以在 \(\Theta(n^2)\) 時間內遞推求出。因此藉助字首和,我們即可做到 \(\Theta(n^2)\) 預處理、\(\Theta(1)\) 查詢。
不妨將上面的字首和矩陣 \(\mathrm{sum}(i, j)\) 看作二維字首和。我們發現,在處理這類字首和的過程中,容斥是一個非常實用的工具。
容斥原理
對於一個 \(n\) 維的陣列 \(a(i_1, i_2, i_3, \cdots, i_n)\),設每一維的值域均為 \([1, m]\)。我們定義其字首和陣列 \(\mathrm{sum}(i_1, i_2, i_3, \cdots, i_n)\) 表示所有滿足 \(1 \leq j_1 \leq i_1, 1 \leq j_2 \leq i_2, 1 \leq j_3 \leq i_3, \cdots, 1 \leq j_n \leq i_n\) 的元素 \(a(j_1, j_2, j_3, \cdots, j_n)\) 的和。那麼問題來了:如何才能快速求出 \(\mathrm{sum}\) 陣列中每個元素的值?
顯然有一個 \(\Theta((m ^ n)^ 2) = \Theta(m ^ {2n})\) 的暴力:我們列舉每一對 \((i_1, i_2, \cdots, i_n), (j_1, j_2, \cdots, j_n)\),判斷是否滿足 \(j_1 \leq i_1, j_2 \leq i_2, \cdots, j_n \leq i_n\),若滿足,則將 \(a(j_1, j_2, \cdots, j_n)\) 累加到 \(a(i_1, i_2, \cdots, i_n)\) 中。顯然這個暴力太不優美了,因為我們發現它列舉了很多無用的狀態;那麼對於每一個固定的狀態 \(i\),考慮將每一維 \(j_k\) 的列舉範圍縮小至 \([1, i_k]\),即可做到所有列舉的狀態都有貢獻,這樣複雜度即可降至 \(\Theta(\Theta(\frac{m ^ {2n}}{2 ^ n})\)。
然而我們發現複雜度仍然不夠優秀,因為相對於 \(\Theta(m ^ n)\) 的輸入量,它還是平方級別的。此時,讓我們回顧一下開頭提到的二維字首和是怎麼做的:首先,對於狀態 \(a(i_1, i_2)\) 它的暴力也是列舉 \(j_1 \in [1, i_1], j_2 \in [1, i_2]\),然後累加貢獻,和這個問題很類似(事實上它就是該問題的一個特例);其次,我們發現它巧妙地應用了字首和陣列 \(\mathrm{sum}\) 本身,將其轉化為了一個容斥問題 \(\mathrm{sum}(i_1, i_2) = a(i_1, i_2) + \mathrm{sum}(i_1 - 1, i_2) + \mathrm{sum}(i_1, i_2 - 1) - \mathrm{sum}(i_1 - 1, i_2 - 1)\),於是每個狀態 \(\mathrm{sum}(i_1, i_2)\) 都可以在 \(\Theta(1)\) 的複雜度內地推求出。
於是在求高維字首和的過程中,我們考慮將上面的容斥套過來。此時你已知 \(a(i_1, i_2, \cdots, i_n)\) 的值;對於其他需要累加的值,我們考慮從 \(\mathrm{sum}\) 陣列中前面的狀態轉移過來,且狀態每一維 \(j_k\) 均為 \(i_k\) 或 \(i_k - 1\)。考慮對每一個這樣的狀態 \(\mathrm{sum}(j_1, j_2, \cdots, j_k)\) 計算其容斥係數,記 \(s_k = i_k - j_k\),那麼根據容斥原理,該項的容斥係數為 \((-1)^{s_1 + s_2 + \cdots + s_k + 1}\)。類比二維字首和理解即可。
考慮其時間複雜度。由於每一個狀態的求解過程都要依賴於它前面的 \(\Theta(2^n)\) 個狀態,因此時間複雜度降至 \(\Theta(m^n \cdot 2^n) = \Theta((2m) ^ n)\)。
然而某些題的資料範圍把 \(m^n\) 開到 \(10^6\) 級別,發現這樣依然無法透過。怎麼辦?
高維字首和
我們回到最簡單的問題,考慮一維字首和是怎麼求的:顯然有 \(\mathrm{sum}(i) = \mathrm{sum}(i - 1) + a_i\),因此直接從左往右掃一遍即可。時間複雜度為 \(\Theta(n)\)。
那麼二維字首和怎麼求呢?根據一維字首和,我們可以得到一個比容斥更簡單的做法。考慮先把每一行的字首和陣列求出來,記 \(s(i, j) = \sum_{k = 1}^j a_{i, j}\) 表示第 \(i\) 行前 \(j\) 個數的和;稍加思考即可發現,當 \(j\) 固定時,\(\mathrm{sum}(i, j)\) 即為 \(s\) 陣列第 \(j\) 列的字首和,因此有 \(\mathrm{sum}(i, j) = \sum_{k = 1}^i s(i, j)\)。
考慮線上地維護這個過程:初始時,令 \(\mathrm{sum}(i, j) = a(i, j)\),只表示 \(a(i, j)\) 這一個位置的“和”。我們將求字首和的過程分為兩輪:我們期望在第一輪結束後,\(\mathrm{sum}(i, j)\) 表示第 \(i\) 行的字首和,那麼只需對每一行做一遍一維字首和即可;期望在第二輪結束後,\(\mathrm{sum}(i, j)\) 表示矩陣的二維字首和,因此再對每一列做一遍字首和即可。時間複雜度為 \(\Theta(n^2)\)。
我們也可以用類似的方法求出高維字首和。具體地,對於一個 \(n\) 維陣列 \(a(i_1, i_2, \cdots, i_n)\),考慮將球字首和的過程分為 \(n\) 輪;我們希望在第 \(t\) 輪結束後,\(\mathrm{sum}\) 陣列表示的含義為:對於第 \(j \in [1, t]\) 維,它表示該維上的字首和;對於第 \(j \in [t + 1, n]\) 維,它僅表示這一維上的值。即第 \(t\) 輪結束後,字首和陣列 \(\mathrm{sum}\) 表示:
比較狀態的含義可知,從第 \(t - 1\) 輪結束到第 \(t\) 輪結束,我們只需要把第 \(t\) 的含義變成該維上的字首和。因此只需固定其他維不變,對第 \(t\) 維做一遍一維字首和即可。同樣類比求二維字首和的過程理解即可。
由於求字首和的過程有 \(n\) 輪,每一輪我們都需要列舉所有 \(m^n\) 種狀態,因此時間複雜度降至 \(\Theta(m^n \cdot n)\)。
注意這種方法只能最佳化求高維字首和的過程,如果我們要求它的某個子空間中所有元素的和,還是隻能老老實實地使用容斥原理。
子集列舉
定義全集 \(I = \{ 0, 1, \cdots, n - 1 \}\),我們給它的每一個子集 \(S \subseteq I\) 賦一個權值 \(a_S\)。定義 \(\mathrm{sum}(S)\) 表示所有 \(S\) 的子集的權值和,即 \(\mathrm{sum}(S) = \sum_{T \subseteq S} a_T\)。那麼如何快速求出 \(\mathrm{sum}\) 陣列呢?
常規的做法是對其進行子集列舉:對的每個狀態 \(\mathrm{sum}(S)\),我們暴力列舉所有 \(S\) 的子集 \(T\),並累加所有 \(a(T)\) 的值,由二項式定理可知時間複雜度為 \(\Theta(3^n)\)。當然你也可以使用容斥,但我們發現此時容斥和直接暴力所需要列舉的狀態數是一樣的,甚至容斥實現起來還要複雜一些。
事實上,我們可以將 \(a_S\) 看成一個 \(n\) 維陣列 \(a(i_0, i_1, i_2, \cdots, i_{n - 1})\),其中每一維 \(i_k\) 表示 \(k\) 這個元素在 \(S\) 中是否出現過;那麼 \(\mathrm{sum}\) 就相當於是 \(a\) 的字首和陣列。考慮把求高維字首和的方法套用過來:我們將整個過程分成 \(n\) 輪,第 \(i \in [0, n)\) 輪對第 \(i\) 維求一遍一維字首和。事實上,這個一維字首和非常特殊,固定其他維不變時,我們只需要把第 \(i\) 位上值為 \(0\) 的狀態累加到值為 \(1\) 的狀態上即可。時間複雜度即可最佳化至 \(\Theta(2^n \cdot n)\)。
這裡附上“子集列舉”與“高維字首和最佳化”的程式碼。
程式碼(子集列舉)
for(i=0;i<(1<<n);i++)
for(j=i;true;j=(j-1)&i){
sum[i]+=a[j];
if(!j) break;
}
程式碼(高維字首和最佳化子集列舉)
for(i=0;i<(1<<n);i++) sum[i]=a[i];
for(i=0;i<n;i++)
for(j=0;j<(1<<n);j++)
if(j>>i&1) sum[j]+=sum[j^(1<<i)];