树分治入门 POJ1741,hdu5977

经常发现一些布布扣一类的网站直接爬csdn的blog,娘的

原文地址在这http://blog.csdn.net/cww97/article/details/77506430

树分治讲解

对于树上的路径问题,一种高效的处理方式就是分治算法。关于树分治算法的研究,详见2009年IOI国家集训队论文——《分治算法在树的路径问题中的应用》。
通常对于树上的分治算法有两种,第一种是针对点进行的分治,另一种是针对边进行的分治,可以证明,大部分情况下点分治算法的性能更加稳定,而边分治在某些情况下,算法效率非常低。所以以下主要讨论点分治。
如POJ-1741,求解一棵树中路径长度不大于K的有多少点对。
对于一棵有根树,树中满足对所对应的一条路径,必然是以下两种情况之一:
1.经过根节点
2.路径在以根节点某个儿子为根的一棵子树中
对于情况2,可以递归求解,下面主要来考虑情况1.
那么对于这道题的情况1,设dis[i]为节点i到根的距离,我们就是要求解有多少对经过根的路径,那么问题等价于能找到多少对不同的点对(i,j),使得dis[i]+dis[j]<=k,而且i和j要属于以当前根的两个不同的儿子为根的子树中。
将问题转化以下,就可以发现所求结果等价于在一棵有根树中找到的点对数x-在以当前根的儿子为根的子树所找到的点对数。
求X、Y的过程均可以转化为以下问题:已知A[1],A[2],…A[m],求满足i

poj1741

论文里第一道例题

参考了网上的两个板

kuangbin的solve写的很csy,这一部分用了这个

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e4 + 7;
const int INF = 0x3f3f3f3f;
int n, k, ans;

struct Edge{
    int from, to, w, nxt;
    Edge(){}
    Edge (int f, int t, int _w, int n):from(f),to(t),w(_w),nxt(n){}
} edges[N * 2];
bool vis[N];
int head[N], E, siz[N], dep[N];

void Init(){
    E = 0;
    memset(head, -1, sizeof(head));
    memset(vis, false, sizeof(vis));
}
void AddEdge(int u,int v,int w){
    edges[E] = Edge(u, v, w, head[u]);
    head[u] = E++;
}

int dfssize(int u, int pre){
    siz[u] = 1;
    for(int i = head[u]; i != -1; i = edges[i].nxt){
        Edge &e = edges[i];
        if(e.to == pre || vis[e.to])continue;
        siz[u] += dfssize(e.to, u);
    }
    return siz[u];
}

//找重心
void dfsroot(int u, int pre, int totnum, int &minn, int &root){
    int maxx = totnum - siz[u];
    for (int i = head[u]; i != -1;i = edges[i].nxt){
        Edge &e = edges[i];
        if(e.to == pre || vis[e.to]) continue;
        dfsroot(e.to, u, totnum, minn, root);
        maxx = max(maxx, siz[e.to]);
    }
    if(maxx < minn){minn = maxx; root = u;}
}

//求每个点离重心的距离
void dfsdep(int u,int pre,int dist, int &num){
    dep[num++] = dist;
    for(int i = head[u]; i != -1; i = edges[i].nxt){
        Edge &e = edges[i];
        if(e.to == pre || vis[e.to])continue;
        dfsdep(e.to, u, dist + e.w, num);
    }
}

//计算以u为根的子树中有多少点对的距离小于等于K
int calc(int u, int d){
    //printf("calc(%d, %d)\n", u, d);
    int ans = 0, num = 0;
    dfsdep(u, -1, d, num);
    sort(dep, dep + num);
    int i = 0, j = num - 1;
    for (; i < j; i++){
        while (dep[i]+dep[j]>k && i<j) j--;
        ans += j - i;
    }
    return ans;
}

void solve(int u){
    int Max = N, root, minn = INF;
    int totnum = dfssize(u, -1);
    dfsroot(u, -1, totnum, minn, root);
    ans += calc(root, 0);
    vis[root] = 1;
    for(int i = head[root]; i != -1; i = edges[i].nxt){
        Edge &e = edges[i];
        if (vis[e.to]) continue;
        ans -= calc(e.to, e.w);
        solve(e.to);
    }
}

int main(){
    ///freopen("in.txt","r",stdin);
    int u, v, w;
    for (; ~scanf("%d%d", &n, &k) && (n|k);){
        Init();
        for(int i = 1; i < n; i++){
            scanf("%d%d%d", &u, &v, &w);
            AddEdge(u, v, w);
            AddEdge(v, u, w);
        }
        ans = 0;
        solve(1);
        printf("%d\n", ans);
    }
    return 0;
}

2016ICPC大连 hdu5977

现场的时候k = 7,树形dp可以水过,重现赛的时候k改为了10

需要树分治,裸的树分治(如上),统计距离小于等于k的点对数

本题问路径经包含所有颜色的点对数

利用位运算状压,k种颜色,k<=10,每种颜色占一位

原题转化为统计路径 or运算和 == fullk的点对数

fullk = (1<<k) - 1;

poj1741通过一个sort和两个指针统计出了dist<=k的点对数

这里是二进制位,sort不灵了,需要另一种nlogn的方案

对于每点u的状态dep[u],枚举其每个子集s0,ans += hash[fullk ^ so]

枚举子集一开始也不会,很玄学,这里有一些讲解,骚到了

这里写图片描述

位运算状压其实很好想,把原来的加号改成or运算就好了

很骚的一段就是calc中的一段,从这里再次盗个图来,举了个枚举子集的例子

这里写图片描述

我见他代码第一行是

/**此代码借鉴了某大神的,我看懂了后又分析的*/

似乎他也参考了这个blog

扯了一堆有的没的

最后贴自己的代码吧,用上题的板子改,dfsdep和dfs看着改就好,calc动些脑子

#include <map>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 5e4 + 7;
const int INF = 0x3f3f3f3f;
int n, k;
LL ans;
int col[N], fullk;
LL hashS[1200];

struct Edge{
    int from, to, w, nxt;
    Edge(){}
    Edge (int f, int t, int _w, int n):from(f),to(t),w(_w),nxt(n){}
} edges[N * 2];
bool vis[N];
//本题中边无w,dep为节点到root col or运算和
int head[N], E, siz[N], dep[N];

void Init(){
    E = 0; fullk = (1<<k) - 1;
    memset(head, -1, sizeof(head));
    memset(vis, false, sizeof(vis));
}
void AddEdge(int u,int v,int w){
    edges[E] = Edge(u, v, w, head[u]);
    head[u] = E++;
}

int dfssize(int u, int pre){
    siz[u] = 1;
    for(int i = head[u]; i != -1; i = edges[i].nxt){
        Edge &e = edges[i];
        if(e.to == pre || vis[e.to])continue;
        siz[u] += dfssize(e.to, u);
    }
    return siz[u];
}

//找重心
void dfsroot(int u, int pre, int totnum, int &minn, int &root){
    int maxx = totnum - siz[u];
    for (int i = head[u]; i != -1;i = edges[i].nxt){
        Edge &e = edges[i];
        if(e.to == pre || vis[e.to]) continue;
        dfsroot(e.to, u, totnum, minn, root);
        maxx = max(maxx, siz[e.to]);
    }
    if(maxx < minn){minn = maxx; root = u;}
}

//求每个点离重心的距离
void dfsdep(int u, int pre, int dist, int &num){
    //printf("u = %d, num = %d, dist = %d\n", u, num, dist);
    dep[num++] = dist;
    for(int i = head[u]; i != -1; i = edges[i].nxt){
        Edge &e = edges[i];
        if(e.to == pre || vis[e.to])continue;
        dfsdep(e.to, u, dist | (1<<col[e.to]), num);
    }
}

//计算以u为根的子树中有多少点对的距离小于等于K, 经过u
LL calc(int u, int d){ //d为基础距离
    //printf("calc(%d, %d)\n", u, d);
    LL ans = 0;
    int num = 0;
    dfsdep(u, -1, d, num);
    memset(hashS, 0, sizeof(hashS));
    for (int i = 0; i < num; i++) hashS[dep[i]]++;
    for (int i = 0; i < num; i++){
        hashS[dep[i]]--;
        ans += hashS[fullk];
        for (int s0 =  dep[i]; s0; s0 = (s0-1) & dep[i]){
            ans += hashS[fullk ^ s0];
        }
        hashS[dep[i]]++;
    }
    return ans;
}

void dfs(int u){
    int root, minn = INF;
    int totnum = dfssize(u, -1);
    dfsroot(u, -1, totnum, minn, root);
    //printf("---------> root = %d\n", root);
    ans += calc(root, 1<<col[root]);
    //printf("ans = %d\n", ans);
    vis[root] = 1;
    for(int i = head[root]; i != -1; i = edges[i].nxt){
        Edge &e = edges[i];
        if (vis[e.to]) continue;
        ans -= calc(e.to, (1<<col[root]) | (1<<col[e.to])); //减去重复部分,即没必要经过u的
        //printf("ans = %d\n", ans);
        dfs(e.to);                             //在下一层dfs中解决
    }
}

int main(){
    //freopen("in.txt", "r", stdin);
    int u, v;
    for (; scanf("%d%d", &n, &k) == 2;){
        Init();
        for (int i = 1; i <= n; i++) {
            scanf("%d", &col[i]);
            col[i]--;
        }
        for (int i = 1; i < n; i++){
            scanf("%d%d", &u, &v);
            AddEdge(u, v, 0);
            AddEdge(v, u, 0);
        }
        ans = 0;
        if (k == 1) ans = (LL)n * (LL)n;
        else dfs(1);
        printf("%lld\n", ans);
    }
    return 0;
}

一个子集枚举的小题目

poj2531,给个20个点的图,问最小割

还有点迷糊这个怎么就能剪枝了

#include <cstdio>
#include <algorithm>
using namespace std;

int main(){
    int n, g[20][20];
    scanf("%d", &n);
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n; j++)
            scanf("%d", &g[i][j]);
    int ans = 0;
    //剪枝就是把i++改成了i+=2,让i总是奇数
    for(int i = 1; i < (1<<n); i += 2){
        int sum = 0;
        for (int j = 0; j < n; j++) if (i & (1<<j)){
            for(int k = 0; k < n; k++)
                if ((~i) & (1<<k)) sum += g[j][k];
        }
        ans = max(ans, sum);
    }
    printf("%d\n", ans);
    return 0;
}

这题正解好像是dfs

不过,随机化乱搞似乎也可以,乱搞好玩啊,here

操作1e5次
对于每一次操作,随机选择一个点,把他放到另一个集合里面去,然后for一遍,算出当前的割值sum,再与记录的最大值ans比较更新

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
int n, g[22][22];
bool go[22];

int main(){
    for (; ~scanf("%d", &n);){
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                scanf("%d", &g[i][j]);

        memset(go, 0, sizeof(0));
        int _ = 1e5, sum = 0, ans = 0;
        for (; _--;){
            int t = rand() % n;
            go[t] ^= 1;
            for (int i = 0; i < n; i++){
                if (go[t] ^ go[i]) sum += g[t][i];
                else sum -= g[t][i];
            }
            ans = max(sum, ans);
        }
        printf("%d\n", ans);
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值