@雅禮集訓01/10 - [email protected] matrix
目錄
@[email protected]
給定一個矩陣。求它的所有子矩陣中本質不同的行的個數之和。
input
第一行,兩個正整數 n, m。
第二行,n * m 個正整數,第 i 個數表示 A[i/m][i mod m]。
保證 n * m <= 10^5, 1 <= A[i][j] <= 10^9
output
輸出一個非負整數表示答案。
sample input
2 2
1 1 1 2
sample output
11
@[email protected]
假如我們列舉矩陣的左右邊界,從上往下掃描行。
假如第 i 行上一個與它相同的行在第 j 行,則它對答案的貢獻,即只考慮它這一行(因為包含其他與第 i 行相同的行已經被統計過了)的子矩陣數量,等於它到下邊界的距離*它到第 j 行的距離。
我們可以只列舉左邊界,再把每一行插入 trie 裡面。這樣我們就可以不用特意去列舉右邊界(因為插入進 trie 的時候就可以順便統計出每一列作為右邊界的貢獻),就可以省去繁雜的字串比較匹配,簡化時間複雜度。
其實是因為右邊界移動時有些之前的資訊可以被保留下來。
再細細品味,可以發現左邊界移動時有些資訊也可以保留下來。
具體的操作而言,可以是先固定左邊界在第一列,往右移動時將根的所有子樹合併成一棵 trie,同時動態維護出答案。
具體到演算法細節,我們在每個結點中維護一個 set 表示包含這個結點所表示的字串的行集合,再維護一個 val 表示這個結點對答案的貢獻。
如果向右移動左邊界,先減去根的所有兒子對答案的貢獻 val,然後隨便選中根的某一個子樹,將其他的子樹向它合併。
如果兩個子樹 A 要向 B 合併,首先要將 A, B 根結點合併成一個結點。對於 A 根結點的某一個兒子,如果 B 沒有則 B 根結點接指標到這個兒子;否則再遞迴合併 A, B 的這一棵子樹。
如果兩個結點 p 和 q 合併,其實最主要的是 p 和 q 的 set 合併,我們採用啟發式合併的方法(小的往大的合)。列舉 p 中的 set 中的每一個行,將這個行插入 q 中的 set,同時求出只包含這一行的子矩陣個數,即在它上面且離它最近的行到它的距離 * 在它下面且離它最近的行到它的距離。
時間複雜度看似很高,實際上總結點數 = 結點大小 = n*m,每次結點合併都會至少減少一個結點,每次子樹合併實際上只有結點合併時才會遍歷這個結點。而結點的合併只會合併同一深度的結點,同一深度的 set 大小之和剛好等於行數 n,又因為我們採用的是啟發式合併,所以每個值最多被合併 log 次。加上 set 的維護是 log 級別的。
所以時間複雜度 O(nlog^2n)(這個 n 是矩陣大小 10^5)。
話說我感覺本題好像不需要子樹的啟發式合併……
@[email protected]
常數很大,本地測試過不了全部資料。可能是 STL 用得太猛了。
#include<set>
#include<map>
#include<cstdio>
#include<algorithm>
using namespace std;
struct node;
typedef set<int> Set;
typedef set<int>::iterator set_it;
typedef map<int, node*>::iterator map_it;
typedef long long ll;
const int MAXN = 500000;
Set pl1[MAXN + 5], *cnt1;
struct node{
map<int, node*>ch;
Set *s; ll val;
}pl2[MAXN + 5], *root, *cnt2, *nw;
ll nwtot; int n, m, x;
void init() {
cnt1 = &pl1[0], cnt2 = &pl2[0];
root = nw = cnt2;
nwtot = 0;
}
node *newnode() {
cnt2++, cnt2->s = (++cnt1), cnt2->s->insert(0), cnt2->s->insert(n+1);
return cnt2;
}
void insert(int id, int x) {
if( !nw->ch.count(x) ) nw->ch[x] = newnode();
nw = nw->ch[x];
set_it it1 = nw->s->lower_bound(id), it2 = it1; it1--;
ll del = 1LL*(id - (*it1))*((*it2) - id);
nw->val += del, nwtot += del;
nw->s->insert(id);
}
void node_merge(node *a, node *b) {
for(map_it it=a->ch.begin();it!=a->ch.end();it++) {
if( b->ch.count(it->first) ) {
node *tmp = b->ch[it->first];
if( tmp->s->size() < it->second->s->size() ) {
swap(tmp->s, it->second->s);
swap(tmp->val, it->second->val);
}
nwtot -= it->second->val;
for(set_it it2=it->second->s->begin();it2!=it->second->s->end();it2++) {
if( !(*it2) || (*it2) == n+1 ) continue;
set_it it3=tmp->s->lower_bound(*it2), it4 = it3; it3--;
ll del = 1LL*((*it2) - (*it3))*((*it4) - (*it2));
nwtot += del, tmp->val += del;
tmp->s->insert(*it2);
}
node_merge(it->second, tmp);
}
else b->ch[it->first] = it->second;
}
}
void trie_merge() {
node *rt = root->ch.begin()->second;
for(map_it it=root->ch.begin();it!=root->ch.end();it++) {
nwtot -= it->second->val;
if( it != root->ch.begin() )
node_merge(it->second, rt);
}
root = rt;
}
inline int read() {
int x = 0; char ch = getchar();
while( ch > '9' || ch < '0' ) ch = getchar();
while( '0' <= ch && ch <= '9' ) x = 10*x + ch-'0', ch = getchar();
return x;
}
int main() {
init(); n = read(), m = read();
for(int i=0;i<n*m;i++) {
if( i % m == 0 ) nw = root;
x = read(); insert(i/m + 1, x);
}
ll ans = nwtot;
for(int i=1;i<m;i++)
trie_merge(), ans += nwtot;
printf("%lld\n", ans);
}
@[email protected]
trie 還能合併,是真的沒想到。
話說我即使不加啟發式合併也能跑得很快。隨機化合並大法好啊。
STL 多起來的確很容易讓人昏昏沉沉的,而且還不好除錯。