• Bio

    关于树型DP和状压DP的一些知识

    bitset<n> s;
    //表示一个n位的二进制数,<>中填写位数;
    a.size()返回大小(位数)    a.count()返回1的个数
    a.any()返回是否有1         a.none()返回是否没有1
    a.set()全都变成1           a.set(p)将第p+1位变成1
    a.set(p, x)将第p+1位变成x  a.reset() 全都变成0
    a.reset(p)将第p+1位变成0   a.flip()全都取反
    a.flip(p)将第p+1位取反     a.to_ulong() 返回它转换为ul的结果,如果超出范围则报错
    a.to_ullong()返回它转换为ull的结果,如果超出范围则报错 a.to_string()返回它转换为string的结果
    a.test() 获得指定位的值
    
    技巧 示例 代码实现
    去掉最后一位 (101101->10110) x >> 1
    在最后加一个0 (101101->1011010) x << 1
    在最后加一个1 (101101->1011011) x << 1 + 1
    把最后一位变成1 (101100->101101) x or 1
    把最后一位变成0 (101101->101100) x or 1 - 1
    最后一位取反 x ^ 1
    把右数第k位变成1 (101001->101101,k=3) x or (1 << (k - 1))
    把右数第k位变成0 (101101->101001,k=3) x and ~(1 << (k - 1))
    右数第k位取反 (101001->101101,k=3) x ^ (1 << (k - 1))
    取末k位 (1101101->1101,k=5) x and (1 << k - 1)
    取右数第k位 (1101101->1,k=4) x >> (k - 1) and 1
    把末k位变成1 (101001->101111,k=4) x or (1 << k - 1)
    末k位取反 (101001->100110,k=4) x ^ (1 << k - 1)
    把右起第一个0变成1 (100101111->100111111) x or (x + 1)
    把右起第一个1变成0 (11011000->11010000) x and (x − 1)
    把右边连续的0变成1 (11011000->11011111) x or (x - 1)
    把右边连续的1变成0 (100101111->100100000) x and (x + 1)
    取右边连续的1 (100101111->1111) (x ^ (x + 1)) >> 1

    状态压缩DP其实是一种暴力的算法,因为它需要遍历每个状态,而每个状态是多个事件的集合,也就是以集合为状态,一个集合就是一个状态。集合问题一般是指数复杂度的NP问题,所以状态压缩DP的复杂度仍然是指数的,只能用于小规模问题的求解。

    为了方便地同时表示一个状态的多个事件,状态一般用二进制数来表示。一个数就能表示一个状态,通常一个状态数据就是一个一串0和1组成的二进制数,每一位二进制数只有两种状态,比如说硬币的正反两面,10枚硬币的结果就可以用10位二进制数完全表示出来,每一个10位二进制数就表示了其中一种结果。

    经典题目:起床困难综合症

    给定 n,mn,m 以及 nn 个数和该数所对应的运算,其中运算有 与、或、异或 三种,问在所有不大于 mm 的非负整数中,对给定的 nn 个数都按该数所对应的运算运算一遍后,能得到得最大的值是多少。AND 表示按位与,OR 表示按位或,XOR 表示按位异或

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,ans=0;
    int main(){
        bitset<40> none,one ;
        none.reset(); // 一个初始化每一位都是0
        one.set();    // 一个初始化每一位都是1
        cin>>n>>m ;
        while(n--){
            string x;int y;
            cin>>x>>y;
            if(x=="AND") none&=y,one&=y;
            else if(x=="OR") none|=y,one|=y;
            else none^=y,one^=y;
        }
        for(int i=0;i<31;i++){
            if(none[i]==1) ans+=(1<<i);  
            //所以如果开始是0最后变成了1,(也就是伤害变多了)一定是需要选取的。
            else if(one[i]==1&&m>=(1<<i))  ans+=(1<<i); 
            //如果开始是1最后变成了1,只要初始伤害值比1<<
            // (1在这一位的位数)大(也就是满足题目条件),显然也是要选取的。
        }
        cout<<ans<<endl;
        return 0;
    }
    

    压缩状态

    把每种状态做离散化标记.

    比如一个灯位于(i,j),灯亮的话light[ i ][ j ]=1;不亮就是0. 这个东西选是1不选是0. 这个点走过就是1没走过就是0等等……

    所以我们把每种状态都可以哈希为一个十进制整数,这个整数根据每个点状态种类的数量不同可以对应转化为二进制、三进制、四进制等等来表示一个状态

    比如二进制:1001,转化为十进制为9,那么状态[ 9 ]就代表着选四号位,不选三号位,不选二号位,选一号位这一种状态

    再假设一个人只有三种状态,吃饭 0,睡觉 1,打东东 2,那么三进制:2101 转化为十进制64,那么状态[ 64 ]就代表着第四个人打东东,第三个人睡觉,第二个人吃饭,第一个人睡觉这样的状态

    二进制操作

    ’&’符号,x&y,会将两个十进制数在二进制下进行与运算(都1为1,其余为0) 然后返回其十进制下的值。例如 3(11)&2(10)=2(10)。

    ’|’符号,x|y,会将两个十进制数在二进制下进行或运算(都0为0,其余为1) 然后返回其十进制下的值。例如3(11)|2(10)=3(11)。

    ’^ ’符号,x ^ y,会将两个十进制数在二进制下进行异或运算(不同为1,其余 为0)然后返回其十进制下的值。例如3(11)^2(10)=1(01)。

    ’~ ’符号,~ x,按位取反。例如~101=010。

    ’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。 ’>>’符号,是右移操作,x>>1相当于给x/2,去掉x二进制下的最右一位.

    1.判断一个数字x二进制下第i位是不是等于1。(最低第1位)

    方法:if(((1<<(i−1))&x)>0) 将1左移i-1位,相当于制造了一个只有第i位 上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0, 说明x第i位上是1,反之则是0。

    2.将一个数字x二进制下第i位更改成1。

    方法:x=x|(1<<(i−1)) 证明方法与1类似。

    3.将一个数字x二进制下第i位更改成0。

    方法:x=x&~(1<<(i−1))

    4.把一个数字二进制下最靠右的第一个1去掉。

    方法:x=x&(x−1)


    一、概述

    1.状态压缩

    状态压缩就是使用某种方法,简明扼要地以最小代价来表示某种状态,通常是用一串01数字(二进制数)来表示各个点的状态。这就要求使用状态压缩的对象的点的状态必须只有两种,0 或 1;当然如果有三种状态用三进制来表示也未尝不可。

    2.使用条件

    从状态压缩的特点来看,这个算法适用的题目符合以下的条件:

    解法需要保存一定的状态数据(表示一种状态的一个数据值),每个状态数据通常情况下是可以通过2进制来表示的。这就要求状态数据的每个单元只有两种状态,比如说棋盘上的格子,放棋子或者不放,或者是硬币的正反两面。这样用0或者1来表示状态数据的每个单元,而整个状态数据就是一个一串0和1组成的二进制数。

    解法需要将状态数据实现为一个基本数据类型,比如int,long等等,即所谓的状态压缩。状态压缩的目的一方面是缩小了数据存储的空间,另一方面是在状态对比和状态整体处理时能够提高效率。这样就要求状态数据中的单元个数不能太大,比如用int来表示一个状态的时候,状态的单元个数不能超过32(32位的机器),所以题目一般都是至少有一维的数据范围很小。

    3.状压DP

    状压DP,顾名思义,就是使用状态压缩的动态规划。

    动态规划问题通常有两种,一种是对递归问题的记忆化求解,另一种是把大问题看作是多阶段的决策求解。这里用的便是后一种,这带来一个需求,即存储之前的状态,再由状态及状态对应的值推演出状态转移方程最终得到最优解。

    二、位运算

    一般基础的状压就是将一行的状态压成一个数,这个数的二进制形式反映了这一行的情况。由于使用二进制数来保存被压缩的状态,所以要用到神奇的二进制位运算操作,将一个十进制数转成二进制进行位运算操作再转回十进制数。

    按位与&(有0为0,其实就是且)
    按位或|(有1为1,其实就是或)
    按位取反~(注意负数补码的符号,最前面的第一位是1)
    异或^(相同为0,不同为1)
    左移<<
    右移>>
    

    一般情况下要预处理前k行(k由题目定)。

    Dp时题目给的条件和fit函数、state数组都要检查。

    最最重要的一点:

    位反(~ )  >  算术  >  位左移、位右移  >  关系运算 
    >  位与  >  位或  >  位异或  >  逻辑运算
    

    所以一般位运算最好打括号。


    1、什么是树型动态规划

    树型动态规划就是在“树”的数据结构上的动态规划,平时作的动态规划都是线性的或者是建立在图上的,线性的动态规划有二种方向既向前和向后,相应的线性的动态规划有二种方法既顺推与逆推,而树型动态规划是建立在树上的,所以也相应的有二个方向:

    叶->根:在回溯的时候从叶子节点往上更新信息

    根 - >叶:往往是在从叶往根dfs一遍之后(相当于预处理),再重新往下获取最后的答案。

    不管是 从叶->根 还是 从 根 - >叶,两者都是根据需要采用,没有好坏高低之分。

    2、树真的是一种特别特别优美的结构!

    用来做动态规划也简直是锦上添花再美不过的事,因为树本身至少就有“子结构”性质(树和子树);本身就具有递归性。所以在树上DP其实是其所当然的事,相比线性动态规划来讲,转移方程更直观,更易理解。

    3、难点

    和线性动态规划相比,树形DP往往是要利用递归+记忆化搜索。

    细节多,较为复杂的树形DP,从子树,从父亲,从兄弟……一些小的要处理的地方,脑子不清晰的时候做起来颇为恶心

    状态表示和转移方程,也是真正难的地方。做到后面,树形DP的老套路都也就那么多,难的还是怎么能想出转移方程,各种DP做到最后都是这样!

    选择节点类

    f[i][0]=f[j][1]f[i][0]=f[j][1]

    f[i][1]=max/min(f[j][0],f[j][1])f[i][1]=max/min(f[j][0],f[j][1])

    树形背包类

    f[v][k]=f[u][k]+valf[v][k]=f[u][k]+val

    f[u][k]=max(f[u][k],f[v][k1])f[u][k]=max(f[u][k],f[v][k−1])


    换根DP,又叫二次扫描,是树形DP的一种。

    其相比于一般的树形DP具有以下特点:

    以树上的不同点作为根,其解不同。

    故为求解答案,不能单求某点的信息,需要求解每个节点的信息。

    故无法通过一次搜索完成答案的求解,因为一次搜索只能得到一个节点的答案。

    难度也就要比一般的树形DP高一点。


    树型DP求直径

    //C++代码
    //采用链式前向星存图
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<climits>
    using namespace std;
    const int N=5e5+100;
    struct edge{
    int u;
    int v;
    int w;
    int next;
    }e[2*N];
    
    int head[N],cnt=0;
    void Insert(int u,int v,int w){
    cnt++;
    e[cnt].u=u;
    e[cnt].v=v;
    e[cnt].w=w;
    e[cnt].next=head[u];
    head[u]=cnt;
    }
    
    int vis[N];
    int  res=INT_MIN;
    
    int d[N],f[N];
    void dfs(int u){
        vis[u]=1;
       for(int i=head[u];i>=0;i=e[i].next){
        int v=e[i].v;
        int w=e[i].w;
        if(!vis[v]){
          dfs(v);
          f[u]=max(max(f[u],d[u]),max(d[v]+w,d[u]+d[v]+w));
          d[u]=max(max(d[u],w),d[v]+w);
        }
       }
    }
    int main(){
        memset(head,-1,sizeof(head));
    int n;
    scanf("%d",&n);
    int u,v,w;
    for(int i=1;i<=n-1;i++){
        scanf("%d%d%d",&u,&v,&w);
        Insert(u,v,w);
        Insert(v,u,w);
    }
    
    dfs(1);
    for(int i=1;i<=n;i++) res=max(res,f[i]);
    printf("%d\n",res);
    return 0;
    }
    
    void dfs(int  x,int pa){
        for(int i=head[x];i;i=edge[i].nxt){
            int  y=edge[i].to;
            if(pa==y) continue ; 
            dfs(y,x);
            maxlen=max(maxlen,dp[x]+dp[y]+edge[i].val);    
            dp[x]=max(dp[x],dp[y]+edge[i].val);
        }
    }
    

    P1352 没有上司的舞会

    某大学有 nn 个职员,编号为 1N1 \sim N。他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 aia_i,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

    #include <algorithm>
    #include <cstdio>
    using namespace std;
    
    struct edge {
      int v, next;
    } e[6005];
    
    int head[6005], n, cnt, f[6005][2], ans, is_h[6005], vis[6005];
    
    void addedge(int u, int v) {  // 建图
      e[++cnt].v = v;
      e[cnt].next = head[u];
      head[u] = cnt;
    }
    
    void calc(int k) {
      vis[k] = 1;
      for (int i = head[k]; i; i = e[i].next) {  // 枚举该结点的每个子结点
        if (vis[e[i].v]) continue;
        calc(e[i].v);
        f[k][1] += f[e[i].v][0];
        f[k][0] += max(f[e[i].v][0], f[e[i].v][1]);  // 转移方程
      }
      return;
    }
    
    int main() {
      scanf("%d", &n);
      for (int i = 1; i <= n; i++) scanf("%d", &f[i][1]);
      for (int i = 1; i < n; i++) {
        int l, k;
        scanf("%d%d", &l, &k);
        is_h[l] = 1;
        addedge(k, l);
      }
      for (int i = 1; i <= n; i++)
        if (!is_h[i]) {  // 从根结点开始DFS
          calc(i);
          printf("%d", max(f[i][1], f[i][0]));
          return 0;
        }
    }
    

    HDU 2196 Computer

    给定一棵有边权的树,求每个点能到达的最远距离。

    #include<iostream>
    #include<vector>
    #include<algorithm>
    using namespace std;
    const int maxn=10005;
    struct node
    {
      int v,w;
      node(){}
      node(int a,int b)
       {v=a;w=b;}
    };
    vector<node> e[maxn];
    int n,dp[maxn][3];
    void dfs1(int s,int fa);
    void dfs2(int s,int fa);
    int main()
    {
       int i,j,a,b;
       while(~scanf("%d",&n))
       {
          for(i=0;i<maxn;i++) e[i].clear();
          fill(dp[0],dp[0]+maxn*3,0);
          for(i=2;i<=n;i++)
          {
            scanf("%d%d",&a,&b);
            e[i].push_back(node(a,b));
            e[a].push_back(node(i,b));
          }
          dfs1(1,-1);
          dfs2(1,-1);
          for(i=1;i<=n;i++) printf("%d\n",max(dp[i][0],dp[i][2]));
       }
      system("pause");
      return 0;
    }
    void dfs1(int s,int fa)  //子树方向的最大距离和次大距离
    {
      int i,v,w;
      for(i=0;i<e[s].size();i++)
       {
         v=e[s][i].v;
         if(v==fa) continue;
         dfs1(v,s);
         w=dp[v][0]+e[s][i].w;
         if(w>=dp[s][0])
          {
            dp[s][1]=dp[s][0];
            dp[s][0]=w;
          }
         else if(w>dp[s][1]) dp[s][1]=w;
       }
    }
    void dfs2(int s,int fa)  //父亲方向的最大距离
    {
      int i,v,w;
      for(i=0;i<e[s].size();i++)
      {
        v=e[s][i].v;w=e[s][i].w;
        if(v==fa) continue;
        if(dp[s][0]==dp[v][0]+w) dp[v][2]=max(dp[s][1],dp[s][2])+w;
        else dp[v][2]=max(dp[s][0],dp[s][2])+w;
        dfs2(v,s);
      }
    }
    

    POJ 1463 Strategic game

    告诉你一棵树,如果一个节点放一个士兵的话,那么这么士兵可以看到这个节点以及和这个节点直接连接的节点,然后问你最少要放多少个士兵可以使得所有的节点都能被看到。

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<sstream>
    #include<algorithm>
    using namespace std;
    const int maxn=1502;
    int d[maxn][2];
    int gra[maxn][maxn];
    int flag[maxn];
    int n;
    void dp(int cur){
        flag[cur]=1;
        if(d[cur][0]>=0||d[cur][1]>=0){
            return;
        }
        int leaf=1;
        for(int i=0;i<n;i++){
            if(gra[cur][i]&&flag[i]!=1){
                leaf=0;
                dp(i);
                if(d[cur][0]==-1)d[cur][0]=0;
                if(d[cur][1]==-1)d[cur][1]=0;
                d[cur][0]+=d[i][1];
                d[cur][1]+=min(d[i][0],d[i][1]);
            }
        }
        d[cur][1]++; 
        if(leaf==1){
            d[cur][0]=0;d[cur][1]=1;
        }
    }
    int main(void){
        while(scanf("%d",&n)){
            memset(flag,0,sizeof(flag));
            memset(gra,0,sizeof(gra));
            memset(d,-1,sizeof(d));
            int x,cnt;
            int num=n;
            while(num--){
                scanf("%d:(%d)",&x,&cnt);
                while(cnt--){
                    int tem;
                    scanf("%d",&tem);
                    gra[x][tem]=1;
                    gra[tem][x]=1;
                }
            }
            dp(x);
            int ans=min(d[x][0],d[x][1]);
            cout<<ans<<endl;
        }
        return 0;
    }
    

    P2014 CTSC1997 选课

    现在有 nn 门课程,第 ii 门课程的学分为 aia_i,每门课程有零门或一门先修课,有先修课的课程需要先学完其先修课,才能学习该课程。

    一位学生要学习 mm 门课程,求其能获得的最多学分数。

    n,m300n,m \leq 300

    $f(u,i,j)=\max_{v,k \leq j,k \leq \textit{siz_v}} f(u,i-1,j-k)+f(v,s_v,k)$

    #include <algorithm>
    #include <cstdio>
    #include <vector>
    using namespace std;
    int f[305][305], s[305], n, m;
    vector<int> e[305];
    
    int dfs(int u) {
      int p = 1;
      f[u][1] = s[u];
      for (auto v : e[u]) {
        int siz = dfs(v);
        // 注意下面两重循环的上界和下界
        // 只考虑已经合并过的子树,以及选的课程数超过 m+1 的状态没有意义
        for (int i = min(p, m + 1); i; i--)
          for (int j = 1; j <= siz && i + j <= m + 1; j++)
            f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]);  // 转移方程
        p += siz;
      }
      return p;
    }
    
    int main() {
      scanf("%d%d", &n, &m);
      for (int i = 1; i <= n; i++) {
        int k;
        scanf("%d%d", &k, &s[i]);
        e[k].push_back(i);
      }
      dfs(0);
      printf("%d", f[0][m + 1]);
      return 0;
    }
    

    「SDOI2017」苹果树

    树上求背包最大收益,每个物品的体积都是11,同一个物品可以取aia_i次,并且取儿子必须取父亲,除此之外,我们还可以免费取一条最长链

    #include<cstdio>
    #include<algorithm>
    #include<vector>
    using namespace std;
    const int N=4*1e4+10;const int K=5*1e5+10;const int NK=6*1e7+10;
    inline void clear(vector <int>& ve){vector <int> emp;swap(emp,ve);}//高速清空函数 
    vector <int> v[N];int w[N];int a[N];int n;int res;int ctt;int k;
    int dfn1[N];int dfn2[N];int df1;int df2;int siz[N];int line[N];
    int dp1[NK];int dp2[NK];bool lf[N];int nfd1[N];int nfd2[N];int T;
    int h;int fa[N];int q1[2*K];int q2[2*K];int hed=1;int til=0;
    inline void dypr(int* dfn,int* dp)//dp的函数 
    {
        for(int i=1;i<=ctt;i++)
        {
            int v=dfn[i];hed=1;til=1;q1[til]=q2[til]=0;//手动inline了全部的deque操作,凑合着看吧 
            for(int j=1;j<=k;j++)
            {
                hed+=(q1[hed]<j-a[v])?1:0;int val=dp[(i-1)*(k+1)+j]-j*w[v];
                dp[i*(k+1)+j]=max(q2[hed]+j*w[v],dp[(i-siz[v])*(k+1)+j]);//单调队列优化转移 
                while(hed<=til&&q2[til]<=val){til--;}q1[++til]=j;q2[til]=val;
            }
        }
    }
    inline void clear_all()//清空函数 
    {
        for(int i=0;i<=ctt;i++){clear(v[i]);lf[i]=line[i]=siz[i]=0;}
        for(int i=0;i<=(ctt+1)*(k+1);i++){dp1[i]=dp2[i]=0;}
        df1=df2=res=ctt=0;h=0;
    }
    void dfs1(int x)//正着dfs,原谅我毒瘤压行 
    {
        siz[x]=1;
        for(int i=0;i<v[x].size();i++)
        {dfs1(v[x][i]);siz[x]+=siz[v[x][i]];}dfn1[++df1]=x;nfd1[x]=df1;
    }
    void dfs2(int x)//reverse后dfs 
    {
        for(int i=v[x].size()-1;i>=0;i--)
        {line[v[x][i]]=line[x]+w[v[x][i]];dfs2(v[x][i]);}
        dfn2[++df2]=x;nfd2[x]=df2;
    }
    inline void solve()
    {
        scanf("%d%d",&n,&k);ctt=n;
        for(int i=1;i<=n;i++){scanf("%d%d%d",&fa[i],&a[i],&w[i]);lf[fa[i]]=true;}
        for(int i=1;i<=n;i++)//加边和拆点 
        {
            v[fa[i]].push_back(i);
        	if(a[i]>1){a[++ctt]=a[i]-1;a[i]=1;w[ctt]=w[i];v[i].push_back(ctt);}
        }line[1]=w[1];dfs1(1);dfs2(1);dypr(dfn1,dp1);dypr(dfn2,dp2);//dfs和dp 
        for(int i=1;i<=n;i++)//枚举叶子更新答案 
        {
            if(lf[i])continue;
            for(int j=0;j<=k;j++)
            {res=max(res,dp1[(nfd1[i]-1)*(k+1)+j]+line[i]+dp2[(nfd2[i]-siz[i])*(k+1)+(k-j)]);}
        }printf("%d\n",res);//然后靠信仰卡常吧 
    }
    int main()
    {scanf("%d",&T);for(int z=1;z<=T;z++){solve();clear_all();}return 0;}//拜拜程序~ 
    

    [POI2008]STA-Station

    给定一个 nn 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。

    #include <bits/stdc++.h>
    using namespace std;
    
    int head[1000010 << 1], tot;
    long long n, sz[1000010], dep[1000010];
    long long f[1000010];
    
    struct node {
      int to, next;
    } e[1000010 << 1];
    
    void add(int u, int v) {  // 建图
      e[++tot] = {v, head[u]};
      head[u] = tot;
    }
    
    void dfs(int u, int fa) {  // 预处理dfs
      sz[u] = 1;
      dep[u] = dep[fa] + 1;
      for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (v != fa) {
          dfs(v, u);
          sz[u] += sz[v];
        }
      }
    }
    
    void get_ans(int u, int fa) {  // 第二次dfs换根dp
      for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (v != fa) {
          f[v] = f[u] - sz[v] * 2 + n;
          get_ans(v, u);
        }
      }
    }
    
    int main() {
      scanf("%lld", &n);
      int u, v;
      for (int i = 1; i <= n - 1; i++) {
        scanf("%d%d", &u, &v);
        add(u, v);
        add(v, u);
      }
      dfs(1, 1);
      for (int i = 1; i <= n; i++) f[1] += dep[i];
      get_ans(1, 1);
      long long int ans = -1;
      int id;
      for (int i = 1; i <= n; i++) {  // 统计答案
        if (f[i] > ans) {
          ans = f[i];
          id = i;
        }
      }
      printf("%d\n", id);
      return 0;
    }
    

    POJ 3585 Accumulation Degree

    给定一棵树。

    · 树的每个边都具有正边权,代表边的容量。

    · 树中度为1的点被命名为出海口。

    · 每个边的流量不能超过容量。

    A(x)A(x)是将点xx视作一个无线喷水机,表示点xx可以流到其他(如果他也是出海口,则排除他)出海口的最大流量。

    你的任务找一个点,使这个最佳最大流量,输出这个值。

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #include <vector>
    #include <cmath>
    using namespace std;
    const int N=200001;
    int t,n,u,v,w,head[N],numE=0,d[N],du[N],dp[N],ans,s;
    struct Edge{
        int next,to,dis;
    }e[2*N];
    void addEdge(int from,int to,int dis){
        e[++numE].next=head[from];
        e[numE].to=to;
        e[numE].dis=dis;
        head[from]=numE;
    }
    int dfs_(int x,int l){
        int p=0;d[x]=0;
        for(int j=head[x];j;j=e[j].next){
            int v=e[j].to;
            if(v==l)continue;
            p+=min(e[j].dis,dfs_(v,x));
        }
        if(du[x]!=1)return d[x]=p; 
        else return e[head[x]].dis;
    }
    void dfs(int x,int l){
        for(int j=head[x];j;j=e[j].next){
            int v=e[j].to;
            if(v==l)continue;
            if(du[x]==1)dp[v]=e[j].dis+d[v];
            else dp[v]=d[v]+min(e[j].dis,dp[x]-min(e[j].dis,d[v]));
            ans=max(ans,dp[v]);
            dfs(v,x);
        }
    }
    int main() {
        scanf("%d",&t);
        while(t--){
            memset(head,0,sizeof(head));
            memset(d,0,sizeof(d));
            memset(dp,0,sizeof(dp));
            memset(du,0,sizeof(du));
            numE=0;ans=0;
            scanf("%d",&n);
            for(int i=1;i<n;i++){
                scanf("%d%d%d",&u,&v,&w);
                addEdge(u,v,w);addEdge(v,u,w);
                du[u]++;du[v]++;
            }
            dp[2]=dfs_(1,-1);
            dfs(1,-1);
            printf("%d\n",max(dp[1],ans));
        }
        return 0;
    }
    

    [USACO10MAR]Great Cow Gathering G

    BessieBessie 正在计划一年一度的奶牛大集会,来自全国各地的奶牛将来参加这一次集会。当然,她会选择最方便的地点来举办这次集会。

    每个奶牛居住在 NN 个农场中的一个,这些农场由 N1N-1 条道路连接,并且从任意一个农场都能够到达另外一个农场。道路 ii 连接农场 AiA_iBiB_i ,长度为 LiL_i 。集会可以在 NN 个农场中的任意一个举行。另外,每个牛棚中居住着 CiC_i 只奶牛。

    在选择集会的地点的时候, BessieBessie 希望最大化方便的程度(也就是最小化不方便程度)。比如选择第 XX 个农场作为集会地点,它的不方便程度是其它牛棚中每只奶牛去参加集会所走的路程之和(比如,农场 ii 到达农场 XX 的距离是 2020 ,那么总路程就是 Ci×20C_i\times 20)。帮助 BessieBessie 找出最方便的地点来举行大集会。

    #include<bits/stdc++.h>
    #define bl(i,a) for(long long i=head[a];i;i=nxt[i])
    using namespace std;
    const int N=2e6+3;
    
    long long nxt[N],edge[N],head[N],dis[N],tot,n,c[N],sum,ans=1e17;
    void add(int x,int y,int w){
    	nxt[++tot]=head[x];
    	head[x]=tot;
    	edge[tot]=y;
    	dis[tot]=w;
    }
    
    long long siz[N],f[N];
    void getsiz(int me,int dad){
    	siz[me]=c[me];
    	bl(i,me){
    		int v=edge[i];
    		if(v==dad)continue;
    		getsiz(v,me);
    		siz[me]+=siz[v];
    		f[me]=f[me]+f[v]+siz[v]*dis[i];
    	}
    }
    
    long long d[N];
    void dp(int me,int dad){
    	bl(i,me){
    		int v=edge[i];
    		if(v==dad)continue;
    		d[v]=1ll*d[me]-siz[v]*dis[i]+(sum-siz[v])*dis[i];
    		ans=min(d[v],ans);
    		dp(v,me);
    	}
    }
    
    int main(){
    	cin>>n;
    	for(int i=1;i<=n;i++){
    		cin>>c[i];sum+=c[i];
    	}
    	for(int i=1,l,r,w;i<n;i++){
    		cin>>l>>r>>w;
    		add(l,r,w);add(r,l,w);
    	}
    	getsiz(1,0);
    	d[1]=f[1];
    	dp(1,0);
    	cout<<ans;
    }
    

    「SCOI2005」互不侵犯

    N×NN\times N 的棋盘里面放 KK 个国王(1N9,1KN×N1 \leq N \leq 9, 1 \leq K \leq N \times N),使他们互不攻击,共有多少种摆放方案。

    国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 88 个格子。

    #include <algorithm>
    #include <iostream>
    using namespace std;
    long long sta[2005], sit[2005], f[15][2005][105];
    int n, k, cnt;
    
    void dfs(int x, int num, int cur) {
      if (cur >= n) {  // 有新的合法状态
        sit[++cnt] = x;
        sta[cnt] = num;
        return;
      }
      dfs(x, num, cur + 1);  // cur位置不放国王
      dfs(x + (1 << cur), num + 1,
          cur + 2);  // cur位置放国王,与它相邻的位置不能再放国王
    }
    
    bool compatible(int j, int x) {
      if (sit[j] & sit[x]) return false;
      if ((sit[j] << 1) & sit[x]) return false;
      if (sit[j] & (sit[x] << 1)) return false;
      return true;
    }
    
    int main() {
      cin >> n >> k;
      dfs(0, 0, 0);  // 先预处理一行的所有合法状态
      for (int j = 1; j <= cnt; j++) f[1][j][sta[j]] = 1;
      for (int i = 2; i <= n; i++)
        for (int j = 1; j <= cnt; j++)
          for (int x = 1; x <= cnt; x++) {
            if (!compatible(j, x)) continue;  // 排除不合法转移
            for (int l = sta[j]; l <= k; l++) f[i][j][l] += f[i - 1][x][l - sta[j]];
          }
      long long ans = 0;
      for (int i = 1; i <= cnt; i++) ans += f[n][i][k];  // 累加答案
      cout << ans << endl;
      return 0;
    }
    

    「NOI2001」炮兵阵地

    给你一张有N×MN \times M的平原和山地的图,只能在平原上放置炮兵,炮兵的攻击范围为5×55 \times 5的十字,求最多能放多少个炮兵?

    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    using namespace std;
    #define ll long long
    
    const int maxn=100+5;
    const int maxm=(1<<10);
    
    int n,m,top,state[maxm],dp[2][maxm][maxm];
    int cur[maxm],ans,w=1;
    char c;
    
    bool ok(int x){
        if(x&(x<<1)) return false;
        if(x&(x<<2)) return false;
        return true;
    }
    
    void init(){
        for(int i=0;i<(1<<m);i++)
        if(ok(i)) state[++top]=i;
    }
    
    bool fit(int x,int k){
        if(x&cur[k]) return false;
        return true;
    }
    
    bool pd(int x,int y){
        if(state[x]&state[y]) return false;
        return true;
    }
    
    int count(int x){
        int ret=0;
        while(x){
            ret++;
            x&=x-1;
        }
        return ret;
    }
    
    template<typename T>void read(T& aa){
        char cc; ll ff;aa=0;cc=getchar();ff=1;
        while((cc<'0'||cc>'9')&&cc!='-') cc=getchar();
        if(cc=='-') ff=-1,cc=getchar();
        while(cc>='0'&&cc<='9') aa=aa*10+cc-'0',cc=getchar();
        aa*=ff;
    }
    
    int main(){
        read(n),read(m);
        for(int i=1;i<=n;i++){
            cur[i]=0;
            for(int j=1;j<=m;j++){
                cin>>c;
                if(c=='H') cur[i]+=(1<<m-j);
            }
        }
        init();
        for(int i=1;i<=top;i++)
        if(fit(state[i],1)) dp[1][i][0]=count(state[i]);
        
        for(int i=1;i<=top;i++){
            if(!fit(state[i],2)) continue;
            int tot=count(state[i]);
            for(int k=1;k<=top;k++){
                if(!fit(state[k],1)) continue;
                if(!pd(i,k)) continue;
                dp[0][i][k]=max(dp[0][i][k],dp[1][k][0]+tot);
            }
        }
        for(int i=3;i<=n;i++,w^=1)
        for(int j=1;j<=top;j++){
            if(!fit(state[j],i)) continue;
            int tot=count(state[j]);
            for(int k=1;k<=top;k++){
                if(!fit(state[k],i-1)) continue;
                if(!pd(j,k)) continue;
                for(int t=1;t<=top;t++){
                    if(!fit(state[t],i-2)) continue;
                    if(!pd(j,t)||!pd(t,k)) continue;
                    dp[w][j][k]=max(dp[w][j][k],dp[w^1][k][t]+tot);
                }
            }
        }
        
        for(int i=1;i<=top;i++){
            if(!fit(state[i],n)) continue;
            for(int j=1;j<=top;j++){
                if(!fit(state[j],n-1)) continue;
                if(!pd(i,j)) continue;
                ans=max(ans,dp[w^1][i][j]);
            }
        }
        cout<<ans;
        return 0;
    }
    

    「USACO06NOV」玉米田 Corn Fields

    农场主John新买了一块长方形的新牧场,这块牧场被划分成MMNN(1M12;1N12)(1 \leq M \leq 12 ; 1 \leq N \leq 12) ,每一格都是一块正方形的土地。John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。John想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?

    #include<cstdio>
    using namespace std;
    const int P = 1e8;
    long long f[14][4400];
    int sta[14][4400],n,m,a[14][14];
    
    void dfs(int u,int x,int s)\\预处理状态
    {
    	if (x > m)
    	{
    		sta[u][++sta[u][0]] = s;
    		return;
    	}
    	dfs(u,x + 1,s);
    	if (a[u][x]) dfs(u,x + 2,s | (1 << x - 1));
    }
    int main()
    {
    	freopen("cowfood.in","r",stdin);
    	freopen("cowfood.out","w",stdout);
    	scanf("%d%d",&n,&m);
    	for (int i = 1; i <= n; i++)
    		for (int j = 1; j <= m; j++)
    			scanf("%d",&a[i][j]);
    	for (int i = 1; i <= n; i++)
    		dfs(i,1,0);
    	for (int i = 1; i <= sta[1][0]; i++) f[1][i] = 1;
    	for (int i = 2; i <= n; i++)
    		for (int j = 1; j <= sta[i][0]; j++)
    			for (int k = 1; k <= sta[i - 1][0]; k++)
    			{
    				if (sta[i][j] & sta[i - 1][k]) continue;\\排除不可能的情况
    				f[i][j] = (f[i][j] + f[i - 1][k]) % P;
    			}
    	long long ans = 0;
    	for (int i = 1; i <= sta[n][0]; i++) ans = (ans + f[n][i]) % P;
    	printf("%lld\n",ans);
    }
    

    ###「AHOI2009」中国象棋

    这次小可可想解决的难题和中国象棋有关,在一个 nnmm 列的棋盘上,让你放若干个炮(可以是 00 个),使得没有一个炮可以攻击到另一个炮,请问有多少种放置方法。大家肯定很清楚,在中国象棋中炮的行走方式是:一个炮攻击到另一个炮,当且仅当它们在同一行或同一列中,且它们之间恰好 有一个棋子。你也来和小可可一起锻炼一下思维吧!

    #include<cstdio>
    #include<cstring>
    #include<cmath>
    #include<cctype>
    #include<cstring>
    #define mod 9999973
    #define int long long
    #define R register
    using namespace std;
    inline  void in(int &x)
    {
    	int f=1;x=0;char s=getchar();
    	while(!isdigit(s)){if(s=='-')f=-1;s=getchar();}
    	while(isdigit(s)){x=x*10+s-'0';s=getchar();}
    	x*=f;
    }
    int n,m,ans;
    int f[108][108][108];
    inline int C(int x)
    {
    	return ((x*(x-1))/2)%mod;
    }
    signed main()
    {
    	in(n),in(m);
    	f[0][0][0]=1;
    	for(R int i=1;i<=n;i++)
    	{
    		for(R int j=0;j<=m;j++)
    		{
    			for(R int k=0;k<=m-j;k++)
    			{
    				f[i][j][k]=f[i-1][j][k];
    				if(k>=1)(f[i][j][k]+=f[i-1][j+1][k-1]*(j+1));
    				if(j>=1)(f[i][j][k]+=f[i-1][j-1][k]*(m-j-k+1));
    				if(k>=2)(f[i][j][k]+=f[i-1][j+2][k-2]*(((j+2)*(j+1))/2));
    				if(k>=1)(f[i][j][k]+=f[i-1][j][k-1]*j*(m-j-k+1));
    				if(j>=2)(f[i][j][k]+=f[i-1][j-2][k]*C(m-j-k+2));
    				f[i][j][k]%=mod;
    			}
    		}
    	}
    	for(R int i=0;i<=m;i++)
    		for(R int j=0;i+j<=m;j++)
    			(ans+=f[n][i][j])%=mod;
    	printf("%lld",(ans+mod)%mod);
    }
    

    「九省联考 2018」一双木棋

    有一个 n×m 的棋盘,两个人轮流下棋。

    一个位置可以落子当且仅当这个位置的左侧和上面都有棋子。

    两个人落在对应的位置会收获各自的贡献值。

    最大化自己的得分减去对方的得分。

    #include<cstdio>
    using namespace std;
    const int P = 1e8;
    long long f[14][4400];
    int sta[14][4400],n,m,a[14][14];
    
    void dfs(int u,int x,int s)\\预处理状态
    {
    	if (x > m)
    	{
    		sta[u][++sta[u][0]] = s;
    		return;
    	}
    	dfs(u,x + 1,s);
    	if (a[u][x]) dfs(u,x + 2,s | (1 << x - 1));
    }
    int main()
    {
    	freopen("cowfood.in","r",stdin);
    	freopen("cowfood.out","w",stdout);
    	scanf("%d%d",&n,&m);
    	for (int i = 1; i <= n; i++)
    		for (int j = 1; j <= m; j++)
    			scanf("%d",&a[i][j]);
    	for (int i = 1; i <= n; i++)
    		dfs(i,1,0);
    	for (int i = 1; i <= sta[1][0]; i++) f[1][i] = 1;
    	for (int i = 2; i <= n; i++)
    		for (int j = 1; j <= sta[i][0]; j++)
    			for (int k = 1; k <= sta[i - 1][0]; k++)
    			{
    				if (sta[i][j] & sta[i - 1][k]) continue;\\排除不可能的情况
    				f[i][j] = (f[i][j] + f[i - 1][k]) % P;
    			}
    	long long ans = 0;
    	for (int i = 1; i <= sta[n][0]; i++) ans = (ans + f[n][i]) % P;
    	printf("%lld\n",ans);
    }
    

    P1272 重建道路

    一场可怕的地震后,人们用 NN 个牲口棚(编号 1N1∼N )重建了农夫 John 的牧场。由于人们没有时间建设多余的道路,所以现在从一个牲口棚到另一个牲口棚的道路是惟一的。因此,牧场运输系统可以被构建成一棵树。John 想要知道另一次地震会造成多严重的破坏。有些道路一旦被毁坏,就会使一棵含有 PP 个牲口棚的子树和剩余的牲口棚分离,John 想知道这些道路的最小数目。

    #include<cstdio>
    #include<iostream>
    #include<algorithm>
    #include<queue>
    #include<cstring>
    
    #define N 310
    
    using namespace std;
    
    inline int read ()
    {
        int f = 1, x = 0;
        char ch;
        do {ch = getchar (); if (ch== '-') f = -1;} while (ch < '0' || ch > '9');
        do {x = x * 10 + ch - '0'; ch = getchar ();} while (ch >= '0' && ch <= '9');
        return f * x;
    }
    
    struct node
    {
    	int u, v, next;
    } edge[N];
    int n, p;
    int cnt, head[N];
    int f[N][N];
    int a[N];
    bool b[N];
    int root;
    int ans = 0x3f3f3f3f;
    
    inline void add (int u, int v)
    {
    	edge[++cnt].v = v;
    	edge[cnt].next = head[u];
    	head[u] = cnt;
    }
    
    inline int dfs (int now)
    {
    	int temp; int sum = 1;
    	for (int i = head[now]; i; i = edge[i].next)
    	{
    		int v = edge[i].v;
    		temp = dfs (v); sum += temp;
    		for (int j = sum; j >= 1; j --)
    			for (int k = 1; k < j; k ++)
    					f[now][j] = min (f[now][j], f[now][j - k] + f[v][k] - 1);
    	}
    	return sum;
    }
    
    int main ()
    {
    	n = read (); p = read ();
    	int x, y;
    	for (int i = 1; i <= n - 1; i ++)
    	{
    		x = read (); y = read ();
    		a[x] ++; b[y] = 1;
    		add (x, y);
    	}
    	memset (f, 0x3f3f3f3f, sizeof (f));
    	for (int i = 1; i <= n; i ++)
    	{
    		if (!b[i]) root = i;
    		f[i][1] = a[i];
    	}
    	dfs (root);
    	ans = f[root][p];
    	for (int i = 1; i <= n ; i ++)
    	{
    		if (f[i][p] < ans) ans = f[i][p] + 1;
    	}
    	printf ("%d", ans);
    	return 0;
    }
    

    P3177 [HAOI2015] 树上染色

    有一棵点数为 nn 的树,树边有边权。给你一个在 0n0∼n 之内的正整数 kk ,你要在这棵树中选择 kk 个点,将其染成黑色,并将其他的 nkn−k 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的收益。问收益最大值是多少。

    tot=k(mk)+(sz[v]k)(nmsz[v]+k)tot=k∗(m−k)+(sz[v]−k)∗(n−m−sz[v]+k)

    f[u][j]=max(f[u][j],f[u][jk]+f[v][k]+tote[i].w)f[u][j]=max(f[u][j],f[u][j−k]+f[v][k]+tot∗e[i].w)

    #include<algorithm>
    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<cctype>
    #define ll long long
    #define gc getchar
    #define maxn 2005
    using namespace std;
    
    inline ll read(){
    	ll a=0;int f=0;char p=gc();
    	while(!isdigit(p)){f|=p=='-';p=gc();}
    	while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
    	return f?-a:a;
    }
    
    struct ahaha{
    	int w,to,next;
    }e[maxn<<1];int tot,head[maxn];
    inline void add(int u,int v,int w){
    	e[tot].w=w,e[tot].to=v,e[tot].next=head[u];head[u]=tot++;
    }
    
    int n,m,sz[maxn];
    ll f[maxn][maxn];
    void dfs(int u,int fa){
    	sz[u]=1;f[u][0]=f[u][1]=0;
    	for(int i=head[u];~i;i=e[i].next){
    		int v=e[i].to;if(v==fa)continue;
    		dfs(v,u);sz[u]+=sz[v];
    		for(int j=min(m,sz[u]);j>=0;--j){   //此处倒序枚举是为了避免重复选取
    			if(f[u][j]!=-1)    //在DP前应先加上当前子节点的子树纯白色的情况,这是下面也倒序枚举的前提
    				f[u][j]+=f[v][0]+(ll)sz[v]*(n-m-sz[v])*e[i].w;
    			for(int k=min(j,sz[v]);k;--k){
    				if(f[u][j-k]==-1)continue;
    				ll val=(ll)(k*(m-k)+(sz[v]-k)*(n-m-sz[v]+k))*e[i].w;   //当前情况下连接子节点的边的贡献
    				f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]+val);
    			}
    		}
    	}
    }
    
    int main(){memset(head,-1,sizeof head);
    	n=read();m=read();
    	if(n-m<m)m=n-m;
    	for(int i=1;i<n;++i){
    		int u=read(),v=read(),w=read();
    		add(u,v,w);add(v,u,w);
    	}memset(f,-1,sizeof f);
    	dfs(1,-1);
    	printf("%lld",f[1][m]);
    	return 0;
    }
    
  • Recent Activities