Pada post kali ini, saya mau cerita sedikit tentang menggunakan struktur data di tree. Sesuai judul, basic. Artinya saya gak akan nulis tentang Heavy-Light Decomposition atau Centroid Decomposition atau sejenisnya. Capek soalnya
Apa Yang Kita Butuhkan?
- Struktur data seperti segment tree atau fenwick tree
- Teknik mencari lowest common ancestor (LCA)
- (?) Pre-order numbering
Pre-order Numbering
Pada tree, ada penelusuran yang disebut pre-order traversal. Kita membutuhkan teknik itu untuk memberi "nomor masuk" dan "nomor keluar" setiap node. "nomor masuk" adalah urutan masuk ke node tersebut, sedangkan "nomor keluar" adalah urutan masuk node yang terakhir dikunjungi pada subtree tersebut. Kurang lebih, contohnya adalah seperti ini:Masuk|Keluar |
Bagaimana cara membuat array in[] dan out[] ? Kita bisa menggunakan cara seperti potongan code berikut:
Nah, terus apa gunanya penomoran itu? Perhatikan, misal kita representasikan tiap node dengan in[] nya. Apa yang bisa kita simpulkan dari in[] dan out[] ? Yup, tiap rentang [in[a], out[a]] merupakan interval suatu subtree! Biasanya saya sebut "gepengin pohonnya". Karena kita sekarang punya interval, artinya kita bisa menggunakan bantuan struktur data seperti segment tree atau fenwick tree!
Pertama-tama, kita harus tahu ciri-ciri string yang bisa dijadikan palindrom. Hal yang penting, kita lihat paritas (gak tau istilah benarnya apaan) tiap karakter string itu. Jika yang ganjil = 0, kita pasti bisa buat palindrome. Jika yang ganjil = 1, kita bisa buat yang paritasnya ganjil itu jadi di tengah string, dan selanjutnya kita bisa membuat palindrom. Selain itu, pasti tidak ada cara.
Jadi, kita cuma perlu tahu paritas tiap karakter dalam satu subtree. Bagaimana caranya? Kita bisa memakai fakta bahwa paritas hanya 0/1, yang bisa kita representasikan menggunakan biner. Artinya, kita bisa membuat bitmask berukuran 26.
Eksekusinya bagaimana? Kita bisa menggunakan point-update untuk mengubah suatu nilai (ingat kita pakai in[] sebagai indeks). Selanjutnya, untuk mengetahui paritas suatu subtree, kita bisa meng-query paritas pada interval subtree tersebut.
Bagaimana cara menggabungkan paritas? Bitmask berukuran 26 bisa direpresentasikan menggunakan int, dan ketika kita menggabungkan, sebenarnya kita melakukan operasi xor.
Contoh solusi:
Ini hanya salah satu aplikasi pre-order + struktur data untuk menyelesaikan persoalan update-query pada subtree. Terkadang, soalnya bisa saja butuh range-update point-query. Intinya, jika anda sudah lancar melakukan operasi-operasi tersebut pada struktur data, anda tinggal latihan sedikit agar bisa memahami penggunaan in[] dan out[]. Oh iya, terkadang juga, anda butuh lebih dari 1 struktur data untuk menyelesaikannya.
Soal yang bisa dijadikan latihan:
SPOJ - Palindrome in Tree (dibahas pada blog ini)
SPOJ - Snail family problems (Ada 2 kasus untuk update!)
SPOJ - Rooted Trees (butuh lebih dari satu fenwick tree)
SPOJ - Salary Management (Agak ribet)
Codeforces - On Changing Tree (lebih simple dari Rooted Trees)
Yang akan saya bahas di sini adalah operasi update dan query, yang memiliki inverse (seperti penjumlahan, xor, perkalian). Trik yang akan saya bahas ini tidak bisa diaplikasikan pada update dan query sepert max dan min.
Saya sebenarnya hampir tidak memiliki contoh soal untuk teknik ini. Teknik ini saya temukan secara tidak sengaja waktu mencoba mengerjakan UVa- Lightning Energy Report. Dulu, ketika Pelatnas, saya mengerjakannya dengan HLD. Namun, ketika nganggur, saya coba mikir apa yang bisa saya lakukan jika ada pertanyaan "berapa nilai node x?" secara online. HLD? Males, ribet. Waktu ngutak-atik, ternyata jika yang ditanya hanya nilai satu node, kita bisa menggunakan fenwick tree saja!
Bagaimana caranya? Pertama-tama, kita harus tahu cara mengupdate path root -- u untuk tiap node u. Caranya? Ingat bahwa interval [in[v],out[v]] menyatakan interval subtree node v. Seandainya kita ingin mengupdate semua nilai ancestor u dan u itu sendiri, apa yang harus kita lakukan?
Coba update(in[u],value). Observasi apa yang terjadi. Seandainya kita melakukan query(in[v],out[v]) untuk mencari nilai node v, maka node-node yang nilainya terkena efek update(in[u],value) hanyalah ancestor dari u serta u itu sendiri!
Resapi baik-baik paragraf sebelumnya.
Kemudian, bagaimana caranya meng-extend ke path u-v untuk sembarang u dan v? Pertama, jelas kita melakukan update(in[u],value) dan update(in[v],value). Oh tidak, pada LCA(u,v), nilainya ketambah 2 kali. Untuk menghilangkan, lakukan update(in[LCA(u,v)],-value). Tapi, ancestor dari
LCA(u,v) juga masih ketambahan value, padahal mereka harusnya tidak kena update. Yaudah, kita lakukan update(in[parent[ LCA(u,v) ] ], -value).
Contoh solusi:
Seperti saya singgung sebelumnya, aplikasi teknik ini sangat jarang (atau saya kurang banyak latihan). Tapi, keuntungan teknik ini adalah, tidak perlu ribet-ribet bikin HLD (yay!)
Soal yang bisa dipakai latihan:
UVa - Lightning Energy Report (dibahas di blog ini)
SPOJ - Tours (reduce dulu jadi tree dengan menggunakan tarjan untuk mencari bridge)
Sepengalaman saya, teknik yang sering membantu itu yang pertama, yang query subtree. Tapi, tidak ada salahnya kan memperbanyak "senjata" untuk mengalahkan soal? :D
See you next time~
int cntr = 0; int in[MAXN]; int out[MAXN]; void dfs(int now,int prv) { in[now] = ++cntr; for(int nxt : adj[now]) { if(nxt == prv) continue; dfs(nxt,now) } out[now] = cntr; }
Bekerja di Subtree
Tadi kita sudah tahu bahwa dengan bantuan dua array, kita bisa mengubah suatu subtree menjadi suatu interval. Langsung ke contoh soal ya, SPOJ - Palindrome in a TreePertama-tama, kita harus tahu ciri-ciri string yang bisa dijadikan palindrom. Hal yang penting, kita lihat paritas (gak tau istilah benarnya apaan) tiap karakter string itu. Jika yang ganjil = 0, kita pasti bisa buat palindrome. Jika yang ganjil = 1, kita bisa buat yang paritasnya ganjil itu jadi di tengah string, dan selanjutnya kita bisa membuat palindrom. Selain itu, pasti tidak ada cara.
Jadi, kita cuma perlu tahu paritas tiap karakter dalam satu subtree. Bagaimana caranya? Kita bisa memakai fakta bahwa paritas hanya 0/1, yang bisa kita representasikan menggunakan biner. Artinya, kita bisa membuat bitmask berukuran 26.
Eksekusinya bagaimana? Kita bisa menggunakan point-update untuk mengubah suatu nilai (ingat kita pakai in[] sebagai indeks). Selanjutnya, untuk mengetahui paritas suatu subtree, kita bisa meng-query paritas pada interval subtree tersebut.
Bagaimana cara menggabungkan paritas? Bitmask berukuran 26 bisa direpresentasikan menggunakan int, dan ketika kita menggabungkan, sebenarnya kita melakukan operasi xor.
Contoh solusi:
#include <bits/stdc++.h> using namespace std; const int N = 100005; int BIT[N]; int in[N],ot[N]; vector<int> adj[N]; int sz; int n,q; char s[N]; // pre-order numbering void dfs(int now,int prv) { in[now] = ++sz; for(int nex : adj[now]) { if(nex == prv) continue; dfs(nex,now); } ot[now] = sz; } // update paritas pakai xor void update(int x,int val) { val = 1 << val; for(; x <= n ; x += x & -x) BIT[x] ^= val; } int query(int x) { int res = 0; while(x) { res ^= BIT[x]; x -= x & -x; } return res; } int main() { scanf("%d",&n); for(int i = 1 ; i < n ; i++) { int u,v; scanf("%d %d",&u,&v); adj[u].push_back(v); adj[v].push_back(u); } dfs(1,0); scanf("%s",s); for(int i = 1 ; i <= n ; i++) update(in[i],s[i - 1] - 'a'); scanf("%d",&q); for(int i = 0 ; i < q ; i++) { int tipe,x; scanf("%d %d",&tipe,&x); if(tipe == 1) { int res = query(ot[x]) ^ query(in[x] - 1); // jika yang ganjil <= 1, maka bisa printf("%s\n", __builtin_popcount(res) <= 1 ? "YES" : "NO"); } else { char ch[3]; scanf("%s",ch); // pertama-tama, "hapus" kemunculan karakter dulu update(in[x],s[x - 1] - 'a'); s[x - 1] = ch[0]; // "tambahkan" kemunculan karakter baru update(in[x],s[x - 1] - 'a'); } } return 0; }
Ini hanya salah satu aplikasi pre-order + struktur data untuk menyelesaikan persoalan update-query pada subtree. Terkadang, soalnya bisa saja butuh range-update point-query. Intinya, jika anda sudah lancar melakukan operasi-operasi tersebut pada struktur data, anda tinggal latihan sedikit agar bisa memahami penggunaan in[] dan out[]. Oh iya, terkadang juga, anda butuh lebih dari 1 struktur data untuk menyelesaikannya.
Soal yang bisa dijadikan latihan:
SPOJ - Palindrome in Tree (dibahas pada blog ini)
SPOJ - Snail family problems (Ada 2 kasus untuk update!)
SPOJ - Rooted Trees (butuh lebih dari satu fenwick tree)
SPOJ - Salary Management (Agak ribet)
Codeforces - On Changing Tree (lebih simple dari Rooted Trees)
Bekerja di Path
Wut? Path? Bukannya cuma bisa pake heavy-light decomposition?Yang akan saya bahas di sini adalah operasi update dan query, yang memiliki inverse (seperti penjumlahan, xor, perkalian). Trik yang akan saya bahas ini tidak bisa diaplikasikan pada update dan query sepert max dan min.
Saya sebenarnya hampir tidak memiliki contoh soal untuk teknik ini. Teknik ini saya temukan secara tidak sengaja waktu mencoba mengerjakan UVa- Lightning Energy Report. Dulu, ketika Pelatnas, saya mengerjakannya dengan HLD. Namun, ketika nganggur, saya coba mikir apa yang bisa saya lakukan jika ada pertanyaan "berapa nilai node x?" secara online. HLD? Males, ribet. Waktu ngutak-atik, ternyata jika yang ditanya hanya nilai satu node, kita bisa menggunakan fenwick tree saja!
Bagaimana caranya? Pertama-tama, kita harus tahu cara mengupdate path root -- u untuk tiap node u. Caranya? Ingat bahwa interval [in[v],out[v]] menyatakan interval subtree node v. Seandainya kita ingin mengupdate semua nilai ancestor u dan u itu sendiri, apa yang harus kita lakukan?
Coba update(in[u],value). Observasi apa yang terjadi. Seandainya kita melakukan query(in[v],out[v]) untuk mencari nilai node v, maka node-node yang nilainya terkena efek update(in[u],value) hanyalah ancestor dari u serta u itu sendiri!
Resapi baik-baik paragraf sebelumnya.
Kemudian, bagaimana caranya meng-extend ke path u-v untuk sembarang u dan v? Pertama, jelas kita melakukan update(in[u],value) dan update(in[v],value). Oh tidak, pada LCA(u,v), nilainya ketambah 2 kali. Untuk menghilangkan, lakukan update(in[LCA(u,v)],-value). Tapi, ancestor dari
LCA(u,v) juga masih ketambahan value, padahal mereka harusnya tidak kena update. Yaudah, kita lakukan update(in[parent[ LCA(u,v) ] ], -value).
Contoh solusi:
#include <bits/stdc++.h> using namespace std; const int N = 50005; const int LOG = 16; vector<int> adj[N]; int anc[N][LOG]; int depth[N]; int in[N], ot[N]; int sz; int BIT[N]; int n,q; void update(int x,int val) { // bisa saja terjadi // kalau ada pemanggilan update parent[root] if(x == 0) return; for(; x <= n ; x += x & -x) BIT[x] += val; } int query(int x) { int ret = 0; while(x) { ret += BIT[x]; x -= x & -x; } return ret; } int getLCA(int u,int v) { if(depth[u] < depth[v]) swap(u,v); int diff = depth[u] - depth[v]; for(int i = 0 ; diff > 0 ; i++) if(diff & (1 << i)) { u = anc[u][i]; diff -= (1 << i); } if(u == v) return u; for(int i = LOG - 1 ; i >= 0 ; i--) if(depth[u] >= (1 << i) && anc[u][i] != anc[v][i]) { u = anc[u][i]; v = anc[v][i]; } return anc[u][0]; } void read() { scanf("%d",&n); for(int i = 1 ; i < n ; i++) { int u,v; scanf("%d %d",&u,&v); // bikin 1-based u++; v++; adj[u].push_back(v); adj[v].push_back(u); } } void build_table(int now,int par) { anc[now][0] = par; depth[now] = depth[par] + 1; for(int i = 1 ; (1 << i) <= depth[now] ; i++) { int papa = anc[now][i - 1]; int grandpa = anc[papa][i - 1]; anc[now][i] = grandpa; } } void dfs(int now,int prv) { in[now] = ++sz; for(int nex : adj[now]) { if(nex == prv) continue; build_table(nex,now); dfs(nex,now); } ot[now] = sz; } void make_tree() { dfs(1,-1); } void update(int u,int v,int val) { int lca = getLCA(u,v); // update u sama v update(in[u],val); update(in[v],val); // hilangin ketambahan 2 kali update(in[lca],-val); int par = anc[lca][0]; // hilangin ketambahan update(in[par],-val); } void solve() { scanf("%d",&q); for(int i = 0 ; i < q ; i++) { int u,v,val; scanf("%d %d %d",&u,&v,&val); // bikin 1-based u++; v++; update(u,v,val); } for(int i = 1 ; i <= n ; i++) printf("%d\n",query(ot[i]) - query(in[i] - 1)); } void reset() { for(int i = 1 ; i <= n ; i++) { adj[i].clear(); BIT[i] = 0; depth[i] = 0; } sz = 0; } int main() { int t; scanf("%d",&t); for(int tc = 1 ; tc <= t ; tc++) { printf("Case #%d:\n",tc); read(); make_tree(); solve(); reset(); } return 0; }
Seperti saya singgung sebelumnya, aplikasi teknik ini sangat jarang (atau saya kurang banyak latihan). Tapi, keuntungan teknik ini adalah, tidak perlu ribet-ribet bikin HLD (yay!)
Soal yang bisa dipakai latihan:
UVa - Lightning Energy Report (dibahas di blog ini)
SPOJ - Tours (reduce dulu jadi tree dengan menggunakan tarjan untuk mencari bridge)
Sepengalaman saya, teknik yang sering membantu itu yang pertama, yang query subtree. Tapi, tidak ada salahnya kan memperbanyak "senjata" untuk mengalahkan soal? :D
See you next time~
0 komentar:
Posting Komentar