0%

本文引用洛谷图床图片可能无法显示,请复制链接至新标签页查看

树可以用来表示物种之间的进化关系。一棵“进化树”是一个带边权的树,其叶节点表示一个物种,两个叶节点之间的距离表示两个物种的差异。现在,一个重要的问题是,根据物种之间的距离,重构相应的“进化树”。

令N={1..n},用一个N上的矩阵M来定义树T。其中,矩阵M满足:对于任意的i,j,k,有M[i,j] + M[j,k] >= M[i,k]。树T满足:

1.叶节点属于集合N;

2.边权均为非负整数;

3.dT(i,j)=M[i,j],其中dT(i,j)表示树上i到j的最短路径长度。

如下图,矩阵M描述了一棵树。

img

树的重量是指树上所有边权之和。对于任意给出的合法矩阵M,它所能表示树的重量是惟一确定的,不可能找到两棵不同重量的树,它们都符合矩阵M。你的任务就是,根据给出的矩阵M,计算M所表示树的重量。下图是上面给出的矩阵M所能表示的一棵树,这棵树的总重量为15。

img

非常巧妙的一道构造题.


题面指出 $对于任意的i,j,k 有M[i,j] + M[j,k] \geq M[i,k]$

那么很显然$M$描述的是两点之间的最短路了,也就是$M[i,j]=M[i,lca(i,j)]+M[j,lca(i,j)]$


我们取出一个叶子节点$a$,将它作为树根.

接下来我们逐个将叶子加入这颗树里.这个叶子一定会产生一个新枝,我们把这个叶子产生的枝加入答案$ans$

对于第二个叶子$b$,显然它只能直接和$a$连一条边,$ans=M[a,b]$

对于第三个叶子$c$,显然它只能加入链$[a,b]$中,根据$M$的信息我们可以算出$c$产生的枝长度$L$为$(M[a,c]+M[b,c]-M[a,b])/2$,将这个数字加入$ans$

对于第四个叶子$d$,事情就没有这么简单了.它有可能加入链$[a,b]$,分枝长度$L_1$;也有可能加入链$[a,c]$,分枝长度$L_2$.我们分类讨论不同情况:

  1. 分支点位于$[a,lca(b,c)]$,则$L_1=L_2$
  2. 分支点位于$[lca(b,c),b]$,则$L_1$为$d$到$lca(b,d)$的距离;$L_2$为$d$到$lca(c,d)即lca(b,c)$的距离.显然$L_1<L_2$.如果我们认为$L$为$L_2$,那么$lca(b,d)到lca(b,c)$的这一段距离就会重复计算(我们在加入$b$的时候就把这一段加入$ans$了).所以$L$为$L_1$
  3. 分支点位于$[lca(b,c),c]$的时候情形同上

综上,对于第四个节点,$L=\min(L_1,L_2)$

……

推广到第$n$个节点,$L=\min{L_i}$

img

img

图片引自TsReaper的博客


代码如下

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <string>
#include <cstring>
#include <cmath>
#define LL int
using namespace std;
LL map_[100][100],len[100],n;
int main(){
        while (1){
                scanf("%d",&n);
                if (n == 0)
                        break;
                else {
                        memset(map_,0,sizeof(map_));
                        memset(len,0x3f,sizeof(len));
                        for (LL i = 1;i <= n;++ i){
                                for (LL j = 1 + i;j <= n;++ j){
                                        scanf("%d",&map_[i][j]);
                                        map_[j][i] = map_[i][j];
                                }
                        }
                }
                LL ans = len[2] = map_[1][2];
                for (LL i = 3;i <= n;++ i){
                        for (LL j = 2;j < i;++ j){
                                len[i] = min(len[i],(map_[i][j] + map_[i][1] - map_[1][j]) >> 1);
                        }
                        ans += len[i];
                }
                printf("%d\n",ans);
        }

        return 0;
}

文结

司令部的将军们打算在NM的网格地图上部署他们的炮兵部队。一个NM的地图由N行M列组成,地图的每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:

img

如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。 现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

是一道经典的状压dp题,很可惜代码丑得自己都看不下去..

设$f(i,S_{self},S_{father})$为考虑前i行,第i行状态为$S_{self}$,第i-1行状态为$S_{father}$时的最优解

设$cnt(S)$为状态$S$上放置炮兵的数量

转移方程为
$$
f(i,S_{self},S_{father})=max{f(i-1,S_{father},S_{grandfather})+cnt(S_{self})}
$$
按顺序枚举$S_{self},S_{father},S_{grandfather}$进行转移,枚举时应当保证三个$S$互不冲突且对于地形合法

注意两点:

  1. 要考虑$S$可以为空集,即该行不放炮兵
  2. 注意滚动数组优化,否则空间不足

代码如下

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <string>
#include <algorithm>
#include <ctime>
#define LL int 
using namespace std;
LL map_[200],dp[2][2000][2000],p = 0;
LL cal(LL se){
        LL cnt = 0;
        while (se){
                se -= se&(-se);
                cnt ++;
        }
        return cnt;
}
int main(){
        LL n,m;
        cin >> n >> m;
        for (LL i = 1;i <= n;++ i){
                map_[i] = 0;
                for (LL j = 1;j <= m;++ j){
                        char al = getchar();
                        while (al != 'P' && al != 'H') al = getchar();
                        map_[i] <<= 1;
                        if (al == 'P')
                                map_[i] |= 1;
                }
        }
        memset(dp,0,sizeof(dp));
        for (LL s = map_[1];s;s = (s - 1) & map_[1]){
                if (((s << 1) & s) || ((s << 2) & s))
                        continue;
                dp[0][0][s] = cal(s);
        }
        for (LL k = 2;k <= n;++ k){
                p = !p;
                for (LL se = map_[k];se!=-1;se = se?(se - 1) & map_[k]:-1){
                        if (((se << 1) & se) || ((se << 2) & se))
                                continue;
                        LL cal_se = cal(se);
                        LL tmp = map_[k - 1]&(~se);
                        for (LL fa = tmp;fa!=-1;fa = fa?(fa - 1) & tmp:-1){
                                dp[p][fa][se] = 0;
                                if (((fa << 1) & fa) || ((fa << 2) & fa))
                                        continue;
                                LL tmp1 = map_[k - 2]&(~fa)&(~se);
                                for (LL gfa = tmp1;gfa!=-1;gfa = gfa?(gfa - 1) & tmp1:-1){
                                        if (((gfa << 1) & gfa) || ((gfa << 2) & gfa))
                                                continue;
                                        dp[p][fa][se] = max(dp[p][fa][se],dp[!p][gfa][fa]+cal_se);
                                }
                        }
                }
        }
        LL ans = 0;
        for (LL se = map_[n];se!=-1;se = se?(se - 1) & map_[n]:-1){
                if (((se << 1) & se) || ((se << 2) & se))
                        continue;
                LL tmp = map_[n-1]&(~se);
                for (LL fa = tmp;fa != -1;fa = fa?(fa - 1) & tmp:-1){
                        if (((fa << 1) & fa) || ((fa << 2) & fa))
                                continue;
                        ans = max(ans,dp[p][fa][se]);
                }
        }
        cout << ans;

        return 0;
}

文结

一道恶心人的毒瘤题:高精度gcd

注意到,在高精度运算中取模是通过重复相减实现的

所以对于高精度gcd,辗转相除法和更相减损术是一样的

那档燃是写更相减损术

这毒瘤题还卡常,所以就搞了几个小把戏:

  1. 把高精度减法直接重载为-=
  2. 采取亿进制(事实上甚至可以写成万亿进制)

不得不说高精度运算是很坑的算法,建议除了构造函数和重载赋值符之外所有成员函数都应该用const修饰

以及亿进制下的输出也是一个很麻烦的事情…

代码如下

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <string>
#include <algorithm>
#include <ctime>
#define LL long long
using namespace std;
struct big_int{
        LL data[25000],len;
        big_int(char *al){
                LL al_len = strlen(al),base = 1;
                data[len = 0] = 0;
                for (LL p = al_len - 1;p >= 0;p --){
                        if (base >= 100000000)
                                ++ len,data[len] = 0,base = 1;
                        data[len] += (al[p] - '0') * base;
                        base *= 10;
                }
                len ++;
                return ;
        }
        void print()const{
                LL len = this->len;
                if (!len) {
                        cout <<"0\n";
                        return ;
                }
                len --;
                LL b = 10000000;
                while((data[len] / b) == 0)
                        b /= 10;
                printf("%lld",data[len]);
                string out = "";
                for (LL i = len - 1;i >= 0;-- i){
                        LL b = 10000000,d = data[i];
                        while(b){
                                out += d / b + '0';
                                d %= b;
                                b /= 10;
                        }
                }
                cout << out << endl;
                return ;
        }
        bool operator < (const big_int &al)const{
                if (len != al.len)
                        return len < al.len;
                for (LL i = len - 1;i >= 0;-- i)
                        if (data[i] != al.data[i])
                                return data[i] < al.data[i];
                return 0;
        }
        big_int operator -= (big_int &al){
                for (LL i = 0;i < al.len;++ i)
                        data[i] -= al.data[i];
                for (LL i = 0;i < len;++ i)
                        if (data[i] < 0)
                                data[i] += 100000000,
                                data[i + 1] -= 1;
                while (data[-- len] == 0 && len > 0);
                len ++; 
                if (len == 1 && data[0] == 0)
                        len = 0;
                return *this;
        }
};
LL low_gcd(LL a,LL b){
        return b?low_gcd(b,a % b):a;
}
int main(){
        char alpha[20000],beta[20000];
        scanf("%s%s",alpha,beta);
        if (strlen(alpha) <= 18 && strlen(beta) <= 18){
                LL al,be;
                sscanf(alpha,"%lld",&al);
                sscanf(beta ,"%lld",&be);
                cout << low_gcd(al,be);
        }
        else{
                big_int al = big_int(alpha),be = big_int(beta);
                while (al < be || be < al){
                        if (be < al)
                                al -= be;
                        else
                                be -= al;
                }
                al.print();
        }
        return 0;
}

文结

Innopolis University scientists continue to investigate the periodic table. There are n·m known elements and they form a periodic table: a rectangle with n rows and m columns. Each element can be described by its coordinates (r, c) (1 ≤ r ≤ n, 1 ≤ c ≤ m) in the table.

Recently scientists discovered that for every four different elements in this table that form a rectangle with sides parallel to the sides of the table, if they have samples of three of the four elements, they can produce a sample of the fourth element using nuclear fusion. So if we have elements in positions (r1, c1), (r1, c2), (r2, c1), where r1 ≠ r2 and c1 ≠ c2, then we can produce element (r2, c2).

img

Samples used in fusion are not wasted and can be used again in future fusions. Newly crafted elements also can be used in future fusions.

Innopolis University scientists already have samples of q elements. They want to obtain samples of all n·m elements. To achieve that, they will purchase some samples from other laboratories and then produce all remaining elements using an arbitrary number of nuclear fusions in some order. Help them to find the minimal number of elements they need to purchase.

先下一个定义:

  • 联通矩阵:若有一个元素组成的集合,集合内任意一个元素都已存在可以由同集合已存在的元素合成.显然这个集合最大时是一个(不一定连续的)矩阵形状,称联通矩阵

观察联通矩阵的性质

  • 若有两联通矩阵A,B.其中分别有两元素a,b.这两个元素在同一行(或同一列)时,两矩阵可以合成一个更大联通矩阵C.C的左边界为A,B左边界较小的一个,C的右边界为A,B右边界较大的一个.上下边界同理
  • 若整张地图共有k个联通矩阵(当然它们两两之间无法合并),则只需添加k-1个元素就可以使整张地图联通

接着逐行模拟合并联通矩阵的过程

  • 考虑前i-1行,若有一个联通矩阵A,包含$J_1,J_2,…,J_k$几个列.第i行存在任意一个元素$(i,J_t),J_t属于J$,则所有$(i,J_1),(i,J_2),…,(i,J_k)$加入矩阵A
  • 考虑前i-1行,若有两个联通矩阵$A={J_{A1},J_{A2},…,J_{Ak}}$,$B={J_{B1},J_{B2},…,J_{Bk}}$,第i行存在两元素$(i,J_{At})$,$(i,J_{Bt})$,$J_{At}属于J_A,J_{Bt}属于J_B$,则将AB合并为一个矩阵
  • 当然以本题规模无法存下整个矩阵.由于我们是逐行枚举的,所以只要存储矩阵中的所有列就可以代表一个包含以上所有行与这些列的交点的联通矩阵了
  • 值得注意的是:用列来表示不会忽略空列,但是会忽略空行.因此应当加上所有空行的数量(显然在一个空行上任意添加一个元素就可以将整个行加入矩阵)
  • 显然以上说的这些应该用并查集维护

以下是代码:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <cmath>
#include <algorithm>
#define llint long long
using namespace std;
struct coo{
    int x,y;
    bool operator < (const coo &be)const{
        if (x != be.x) return x < be.x;
        else return y < be.y;
    }
}arr[400000];
//set
int fa[400000];
int set_(int k){
    if (k == fa[k]) return k;
    else return fa[k] = set_(fa[k]);
}
int merge(int k,int l){
    fa[set_(k)] = set_(l);
    return set_(k);
}
int main (){
    int n,m,k,ans = 0;
    cin >> n >> m >> k;
    for (int i = 1;i <= k;++ i){
        scanf("%d%d",&arr[i].x,&arr[i].y);
    }
    sort(arr + 1,arr + 1 + k);
    for (int i = 1;i <= m;++ i)
        fa[i] = i;
    int p = 0,cnt = 0;
    bool ifcnt = 0;
    for (int i = 1;i <= n;++ i){
        int f = 0;
        while (++p <= k && arr[p].x == i){
            ifcnt = 1;
            int &y = arr[p].y;
            if (!f) f = set_(y);
            else merge(y,f);
        }
        if (!ifcnt) ++ cnt;
        ifcnt = 0;
        p --;
    }
    int f = set_(1);
    for (int i = 2;i <= m;++ i){
        if (set_(f) != set_(i)) {
            ++ ans;
            f = merge(f,set_(i));
        }
    }
    cout << ans + cnt;
    return 0;
}

以上.

上午的训练结束了,THU ACM小组集体去吃午餐,他们一行N人来到了著名的十食堂。这里有两个打饭的窗口,每个窗口同一时刻只能给一个人打饭。由于每个人的口味(以及胃口)不同,所以他们要吃的菜各有不同,打饭所要花费的时间是因人而异的。另外每个人吃饭的速度也不尽相同,所以吃饭花费的时间也是可能有所不同的。

THU ACM小组的吃饭计划是这样的:先把所有的人分成两队,并安排好每队中各人的排列顺序,然后一号队伍到一号窗口去排队打饭,二号队伍到二号窗口去排队打饭。每个人打完饭后立刻开始吃,所有人都吃完饭后立刻集合去六教地下室进行下午的训练。

现在给定了每个人的打饭时间和吃饭时间,要求安排一种最佳的分队和排队方案使得所有人都吃完饭的时间尽量早。

假设THU ACM小组在时刻0到达十食堂,而且食堂里面没有其他吃饭的同学(只有打饭的师傅)。每个人必须而且只能被分在一个队伍里。两个窗口是并行操作互不影响的,而且每个人打饭的时间是和窗口无关的,打完饭之后立刻就开始吃饭,中间没有延迟。

现在给定N个人各自的打饭时间和吃饭时间,要求输出最佳方案下所有人吃完饭的时刻。

显然吃饭慢的应该先排队,满足贪心

可以设计状态dp(l1,l2)表示队伍长度分别为l1,l2时的最短时间

但是注意到两个队伍的长度是分别变化的,所以状态应当同时维护两队的进食总时间

转移方程见代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <algorithm>
#include <cmath>
#define llint long long
using namespace std;
struct xxx{
    int a,b;
    bool operator < (const xxx &be)const{
        return b > be.b;
    }
}arr[300];
struct yyy{
    int p1,p2;
}___[100000],*dp = ___ + 50000;
int main (){
    int n;
    cin >> n ;
    for (int i = 1;i <= n;++ i)
        scanf("%d%d",&arr[i].a,&arr[i].b);
    sort (arr + 1,arr + 1 + n);
    for (int i = -45000;i < 45000;++ i)
        dp[i].p1 = dp[i].p2 = 0x3f3f3f3f;
    dp[0] = yyy{0,0};
    int sum_ = 0;
    for (int i = 1;i <= n;++ i){
        sum_ += arr[i].a;
        for (int l1 = sum_;l1 >= 0;-- l1){
            int l2 = sum_ - l1;
            int 
                tmp1_p1 = max(dp[l1 - arr[i].a].p1,l1 + arr[i].b),
                        tmp1_p2 = dp[l1 - arr[i].a].p2, 
                        tmp2_p1 = dp[l1].p1,
                        tmp2_p2 = max(dp[l1].p2,l2 + arr[i].b);
            if (max(tmp1_p1,tmp1_p2) < max(tmp2_p1,tmp2_p2)){
                dp[l1] = yyy{tmp1_p1,tmp1_p2};
            }
            else {
                dp[l1] = yyy{tmp2_p1,tmp2_p2};
            }
        }

    }
    int ans = 0x3f3f3f3f;
    for (int i = 0;i <= sum_;++ i)
        ans = min(max(dp[i].p1,dp[i].p2),ans);
    cout << ans;
    return 0;
}

以上.