0、题目链接:http://www.lydsy.com/JudgeOnline/problem.php?id=3052
1、题目大意:给定一颗n个点的无根树,每个点有一个颜色,要进行q次操作,有两种操作,颜色总数是m。
a) Query操作,给定起始点和终点,对于这条路径,从起始点出发,对于沿途的点,如果这个点的颜色j是第i次出现,那么对于这个询问的答案的贡献是vi∗wj。
b) Change操作,每次修改一个节点的颜色。
——截图来自uoj
2、解题思路:
a)这道题在BZOJ上的时间限制是200s,那么对于前3个点,我们可以每次查询在树上dfs,修改直接改颜色。时间复杂度O(n2),期望得分30。
b)对于第四个和第五个点,我们发现m相对较小而且给出的是一个链,还没有修改操作,那么我们可以将这个链看成一个序列,并对这个序列进行分块,每块的大小是,考虑莫队算法,我们将需要查询的区间排序,第一关键字按照左端点的块,第二关键字我们按照右端点的编号排序,每次我们进行暴力的转移,这样,时间复杂度是O(n−−√),为什么呢?因为转移分两种,要么都在这个n−−√的块中转移,转移n次,时间复杂度是O(n−−√),要么是这个块中转移到另外一个块,每次转移是O(n),最多会有n−−√次转移,时间复杂度也是O(n−−√),于是总体的时间复杂度是O(n−−√)。
i.说了这么多,也没个代码= =,莫队算法模板题:BZOJ2038 小Z的袜子
sort(q + 1, q + m + 1, cmp); //对询问进行排序
for(int i = 1; i <= m; i ++){
while(r < q[i].r) {
//右端点向右转移,更新答案
}
while(r > q[i].r) {
//右端点向左转移,更新答案
}
//实际上就是将r->q[i].r
while(l < q[i].l) {
//左端点向左转移,更新答案
}
while(l > q[i].l) {
//左端点向右转移,更新答案
}
//实际上就是将l->q[i].l
//计算这次询问的答案
}
加上a)算法,期望得分50。
c)对于第六和第七个点,我们能看出和b)解法唯一区别就是给点的树不是链了,那么b)中的算法能不能在这里应用呢?序列能够分块,树能不能分块呢?如何分块复杂度才能有保证呢?
i.我们先来看《手把手教你树分块系列》的BZOJ1086 王室联邦。这个题如何解决呢?我们先来陈述一下做法,我们用DFS去分块,我们记录一个栈,当遍历完某个节点的所有子树后,我们将这个节点加入栈,每次遍历完一棵子树,以这个节点为祖先的栈里的元素个数已经超过了我们规定的块的大小B了,就把这些节点变成一个块,将这些元素从栈里弹出,最后将栈中的残留节点归结到最后一个块中。
ii.那么可以确定的是每棵子树对这个节点的贡献不过超过B,为什么呢?因为如果这个子树中的节点数已经超过B了,那么这棵子树本身就会成为一个块。既然我们确定了每棵子树对这个节点的贡献不会超过B,那么我们每遍历完一个子树就检查一次,所以每个块的范围在[B,2B]之间,那么为什么给定的范围强行是[B,3B]呢?因为最后剩余的还要加进最后一个块,这样构造我们可以发现每个块中的节点都是相邻的,这样在块中的两点间距离的上界就是n−−√了
这样复杂度就有保证。
好了,我们会树分块了,莫队算法怎么在树上应用呢?其实是一样的,我们对于一个询问操作记录起始点和终点,每次l -> q[i].l,r -> q[i].r,排序依旧是那样的。
sort(q + 1, q + m + 1, cmp); //对询问进行排序
for(int i = 1; i <= m; i ++){
//什么?这怎么转移啊
while(l != q[i].l) {
//不断的向上跳
//这样简单了啊
}
while(r != q[i].r) {
//不断的向上跳
//这样简单了啊
}
//算出这次询问的答案= =
}
时间复杂度
加上算法a)和算法b),期望得分70。
非常好= =,我们已经拿到70分了。
d) 同样,我们观察第八和第九个测试点,我们可以发现差别在于这里有修改操作了,好!我们如何处理修改操作呢?然后我们发现如果在b)算法中强行安上修改那么时间复杂度会变成O(n2),是不能AC这两个测试点的,那么怎么AC呢?考虑在原来的莫队中加上一维,表示修改的操作时间,如果加上一维,我们块的大小就要变成O(n23)了,排序是前两维按照块的编号排序,最后一维从小到大排序。
我们在转移的时候就变成的O(n23)了,于是转移的时间复杂度就是O(n∗n23)了,具体证明参见b)算法的证明过程。那么我们来算一算第三维转移的时间复杂度,我们发现对于前两维,每一维都有个n13块,于是就有n23种情况,第三维的转移每次都是O(n)的,所以转移的时间复杂度就是O(n∗n23)了,综上所述,总的时间复杂度是O(n∗n23),能通过这两个测试点。
综合以上四个思路,我们可以拿到90分了,算法已经很优秀了。
e) 看到上面四个解法,我们思考一下最后这个点的解法,综合解法c)和解法d)
Vfleaking的解法,按照这个解法,我们发现最后我们只需要暴力的转移即可,我们 发现最后这个式子是我们想要的答案,于是我们将d)的解法转移到树上!我们就 可以拿到100分了!
附上本弱的代码
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
#define M 100100
#define LL long long
inline int read(){
char ch = getchar(); int x = 0, f = 1;
while(ch < '0' || ch > '9'){
if(ch == '-') f = -1;
ch = getchar();
}
while('0' <= ch && ch <= '9'){
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
struct Edge{
int u, v, next;
} G[M * 2];
int head[M], tot;
int first[M];
int n, m, q;
int v[M], w[M], col[M], cnt[M];
int blo[M];
int st[M], Top;
int height[M], ft[M][20];
int B, cnt_;
bool vis[M];
LL ansnow, ans[M];
struct node{
int x, y, z;
} qr[M];
struct Node{
int x, y, z, id;
LL ans;
inline void sw(){
if(blo[x] > blo[y]) swap(x, y);
}
inline bool operator < (const Node& rhs) const{
if(blo[x] != blo[rhs.x]) return blo[x] < blo[rhs.x];
if(blo[y] != blo[rhs.y]) return blo[y] < blo[rhs.y];
return z < rhs.z;
}
} Q[M];
inline void add(int a, int b);
inline void update(int x);
inline void modify(int x, int y);
inline void moveto(int x, int y);
inline void dfs(int x, int fa, int h);
inline void init();
inline int lca(int a, int b);
inline void getans(int x, int y);
int main(){
n = read(); m = read(); q = read();
memset(head, -1, sizeof(head));
for(int i = 1; i <= m; i ++) v[i] = read();
for(int i = 1; i <= n; i ++) w[i] = read();
for(int i = 1; i < n; i ++){
int a = read(), b = read();
add(a, b); add(b, a);
}
B = pow(n, 2.0/3.0);//确定块的大小
for(int i = 1; i <= n; i ++) col[i] = read(), first[i] = col[i];
//我们用链表记录每个点修改前和修改后的真实颜色
dfs(1, 0, 0); while(Top) blo[st[Top --]] = cnt_; init();
//对树进行分块
int totqr = 0, totQ = 0;
for(int i = 1; i <= q; i ++){
int t = read();
if(t == 0){
qr[++ totqr].x = read();
qr[totqr].y = first[qr[totqr].x];
qr[totqr].z = first[qr[totqr].x] = read();
}
else{
Q[++ totQ] = (Node){read(), read(), totQ, totqr};
Q[totQ].sw(); //一个小小的常数优化
}
}
sort(Q + 1, Q + totQ + 1);
int tim = 1;
while(tim <= Q[1].id){ //转移第三维
modify(qr[tim].x, qr[tim].z);
tim ++;
}
moveto(Q[1].x, Q[1].y);getans(1, lca(Q[1].x, Q[1].y));
for(int i = 2; i <= totQ; i ++){
while(tim <= Q[i].id) modify(qr[tim].x, qr[tim].z),tim ++;
while(tim > Q[i].id + 1) modify(qr[tim - 1].x, qr[tim - 1].y), tim --;
//转移第三维
moveto(Q[i - 1].x, Q[i].x); moveto(Q[i - 1].y, Q[i].y); getans(i, lca(Q[i].x, Q[i].y));
//xor一波 顺便求一下答案
}
for(int i = 1; i <= totQ; i ++) printf("%lld\n", ans[i]);
return 0;
}
inline void add(int a, int b){
G[++ tot] = (Edge){a, b, head[a]};
head[a] = tot;
}
inline void update(int x){
//将一个点从集合中删除
if(vis[x]) ansnow -= (LL)w[cnt[col[x]] --] * v[col[x]];
else ansnow += (LL)w[++ cnt[col[x]]] * v[col[x]];
vis[x] ^= 1;
}
inline void modify(int x, int y){
//修改一个节点的颜色
if(vis[x]) update(x), col[x] = y, update(x);
else col[x] = y;
}
inline void moveto(int x, int y){
//转移,并不需要bfs之类的,两个点直接往上跳
while(x != y){
if(height[x] < height[y]) swap(x, y);
update(x); x = ft[x][0];
}
}
inline void dfs(int x, int fa, int h){
//树分块
int bt = Top;
height[x] = h;
ft[x][0] = fa;
int ot = x;
for(int i = head[x]; i != -1; i = G[i].next) if(G[i].v != fa){
dfs(G[i].v, x, h + 1);
if(Top - bt >= B){
cnt_ ++;
while(Top != bt){
blo[st[Top --]] = cnt_;
}
}
}
st[++ Top] = x;
}
inline void init(){
//预处理倍增数组
for(int i = 1; i <= 18; i ++){
for(int j = 1; j <= n; j ++){
ft[j][i] = ft[ft[j][i - 1]][i - 1];
}
}
}
inline int lca(int a, int b){
//求出lca
if(height[a] < height[b]) swap(a, b);
int t = height[a] - height[b];
for(int i = 18; i >= 0; i --){
if(t & (1 << i)){
a = ft[a][i];
}
}
if(a == b) return a;
for(int i = 18; i >= 0; i --){
if(ft[a][i] != ft[b][i]){
a = ft[a][i];
b = ft[b][i];
}
}
return ft[a][0];
}
inline void getans(int x, int y){
//获取答案,一定要将y这个点去掉,因为y这个点所象征的边不在路径上
update(y);
ans[Q[x].z] = ansnow;
update(y);
}