欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Nordic Collegiate Programming Contest 2019 部分题解

程序员文章站 2022-07-15 16:07:18
...

Nordic Collegiate Programming Contest 2019 部分题解

前言,做国外的比赛感觉好像难度上比国内同时期的比赛要简单一点,但是在这种情况下出线的国内的队伍却不能轻松wf捧杯,感觉有点迷惑,可能和国内竞赛环境内卷严重有关。以后训练尽量选择一些正常的题目,避免无意义的trick和怪题,否则只能增强应试技巧,而对自身水平提升无益。

Flow Finder

题意

给定一棵有根树,树上每个点有一个点权。其中一些点的初始值已知,一些位置,现在把所有的未知点点权赋值(要求每个点都是正整数),并且满足:每个点的权值等于其每个孩子的权值只和。特别地,如果方案不唯一或者没有可行方案,输出impossible,否则输出这棵树。

分析

这题可以本质上可以被归纳为构造题。一个通用的解题思路是:先判断在什么情况下有解,然后在排除无解情况后进行构造,这样可以大大降低思维和编码的难度。

先分析何时输出impossible。首先是有多个解的情况,不难发现,如果存在一条链从树的叶子节点到根节点,他们的初始值都未知,那么可以有无限种解决,因为可以对他们同加同减。(这一步我看完题就发现了,这个主要基于对树结构的认识,如果不能凭感觉发现的话,可以画几棵树感受一下。)这说明如果存在“一根直肠通大脑,学多少忘多少”,那么一定有无穷多组解,这是一个充分条件。

Nordic Collegiate Programming Contest 2019 部分题解
那么在没有“直肠”的情况下,是否还有可能有无穷多组解呢?如果没有“直肠”,那么一定存在一个界面,把根节点和所有叶子节点分来,并且这个界面上所有点都已知。

Nordic Collegiate Programming Contest 2019 部分题解
显然,在有"界面"的情况下, 界面以上的点权都变成确定的(也有可能出现冲突,事实上这构成了无解的情况,但是无论如何界面以上的任何结果不会产生多解)。只需要考虑界面下方。那么能否从这个界面去把他的孩子也都确定下来呢?不难发现,如果存在一个点为根,存在至少两个孩子可以一路未知地通向叶子节点,那么就有可能出现无穷多组解。

Nordic Collegiate Programming Contest 2019 部分题解
如上图,紫色是一个已知的界面,红色表示初始未知的节点,在这种情况下,因为两个孩子可以随意分配,所以可能出现无穷解的情况。但是有一种例外情况,当这些红色点的公共紫色父亲的值恰好等于这些红色点覆盖到的叶子节点数量和其他与这些红色节点相邻的已知值之和时,结果是唯一的。 如上图所示,如果这些红色节点的父亲是2+x(x为最下层未被标红的点的值)。这样两个红色的叶子节点的值必定是1。

“界面”

如果上面那个描述有些拗口,那么我们现在研究满足一个点的权值等于其孩子之权值和的树有何性质。因为这个性质具有传递性,如果把一个节点分解为其若干个孩子,那么可以将其孩子再进行拆分。通过不停重复该操作,发现无论如何操作,都能得到一个“界面”。

我们现在给出界面的一个更加严格的定义:

我们称在有根树 TT 上的点集 SS 是关于点 vv 的一个界面,当且仅当:

  • sS\forall s\in S, ss 在以 vv 为根的子树中
  • 对任意在以 vv 为根的子树中的叶子节点 xxsS\exists s\in Sxx 在以 ss 为根的子树中
  • x,yS\forall x,y\in S, xx 不在以 yy 为根的子树中

事实上,在这里,我们认为界面是之前“界面”定义下所有可行点集的一个极小值,最小性由定义中的第三条保证(因为如果一个点的在界面中,那么以它为根的子树都已经被挡住了,他的孩子一定是一个冗余项)。

我们可以发现界面的一个性质:

如果 SS 是关于点 vv 的一个界面,且在这棵树上满足一个点的权值等于其孩子之权值和,那么 vv 的权值等于 SS 中所有点的权值之和。

根据上述拆分的操作,用数学归纳法容易证明。

Nordic Collegiate Programming Contest 2019 部分题解
其中每个颜色代表一个界面(红色也可以被认为是一个界面),并且各界面点权值和相等。

重新分析

在得到界面定义后,我们不难发现,对于任意一个未知的点,如果存在一个关于他的界面上所有点都已知,那么这个点就可以被推断出来。(除非发生冲突,那么就无解)。重复这个操作若干次,使得所有可以通过以上方法求解的点都求出来。

剩下的未知点找不到一个界面,满足其上所有点都已知,那么就无法通过上述方法求解。如果一个点找不到界面,那么一定存在一条连向叶子节点的每个点都未知的边(不然就存在关于它的界面了)。

我们希望唯一确定这些未知点的值。对于一个点,如果我们找不到关于它的界面,那么我们希望去找到一个点,使得它在这个界面中,并且满足

  • 这是界面中唯一一个未知点
  • 或者,界面中所有未知点都是叶子界面,并且他们的值可以被唯一确定为1

反过来考虑,在这一步时(即能找到对应已知界面确定的点都已经被确定),我们将任何一个有未知后继的点拆分为其所有孩子,然后递归地拆分他的所有未知孩子。这样会产生一个关于他的一个界面,由已知点和未知叶子节点组成。如果只有一个未知叶子节点,那么这个未知叶子节点可以被唯一确定;如果有多个未知节点,那么这些未知节点个数必定等于这颗子树的根节点权值减去所有界面中已知点的权值,并且这些未知叶子节点也可以被唯一确定出来。完成所有叶子节点的构造后,其他所有未知节点都被构造出来(因为这样他的未知祖先都能找到对应的界面了)。上述构造过程即证明了解的唯一性。在构造完成后没有发生冲突,即证明了解决的可行性。所以答案正确。

对于不能由上述两种方法构造出来的点,不难发现他们会产生多组解。

结论

我们重新归纳输出impossible的情况:

  • 无解
    • 存在一个点,存在之上两个界面上点已知,并且他们的权值和不同
  • 无穷组解
    • 对根节点不存在界面(直肠)
    • 存在一个点,其值已知,并且存在关于这个点的一个界面(除了这个点自身构成的界面),界面以下的点都已知,并且该点的点权不等于界面上未知点的数量加上已知点的点权和。

对于其他所有情况,都可以构造出来。

实现

按照前文的构造,一种显而易见是进行三次dfs。第一次进行dfs,如果某个节点所有后继已知,那么这个节点也可以被唯一确定,用类似树上dp的手段求解即可,在此过程种顺便判断是否产生冲突。第二次进行dfs,求出所有叶子节点。第三次dfs直接调用第一次dfs的函数即可。

但是不难发现,利用前文构造的手段,后面两次dfs可以一次性实现,并且程序上更加简洁。

对于每个节点,保存三个量:

  • pip_i 表示该点确定的值,如果为止则为0
  • sis_i 表示该点所有已知的孩子的权值和
  • ssiss_i 表示该点在不断拆分未知孩子直到叶子的情况下产生界面上已知节点的权值和

我们在第一次dfs的时候预处理这三项,顺便处理冲突。

void dfs1(int u){
    for(auto v:g[u]){
        dfs1(v);
        if(p[v]){//如果孩子已知(包括被构造出来的已知)
            s[u]+=p[v];//已知孩子
            ss[u]+=p[v];//已知孩子一定在拆分界面中,因为根据定义他们不会被拆分
        }else{
            pre[u].push_back(v);//记录未知孩子
            ss[u]+=ss[v];//“拆分界面已知权值和”可以分治
        }
    }
    if(g[u].empty()){//如果是叶子节点
        ss[u]=1;//那么对他的祖先的贡献只可能是1
    }else if(pre[u].empty()){//如果不是叶子节点,且没有未知后继
        if(p[u]&&p[u]!=s[u]){//那么他的权值唯一,判断是否冲突
            cout<<"impossible\n";
            exit(0);
        }
        if(!p[u])p[u]=s[u];//不冲突的话他的权值也确定了,就是他的所有孩子之和(因为都已知)
    }
}

然后第二次dfs构造其他未知点,每个已知的父亲可以直接求出其所有未知孩子。

void dfs2(int u){
    if(!p[u]){//如果走到某个点的时候还未知,那么说明有无穷组解
        cout<<"impossible\n";
        exit(0);
    }
    if(pre[u].size()==1){//如果只有一个未知孩子
        int v=pre[u][0];
        p[v]=p[u]-s[u];//那么就应该是父亲的已知值减去他的所有孩子的值
        if(p[v]<=0){//如果不是正整数则冲突
            cout<<"impossible\n";
            exit(0);
        }
    }
    if(pre[u].size()>1){//如果有多个未知孩子
        if(ss[u]!=p[u]){//那么因为往下衍生得到的未知叶子只能是1,所以直接判断这个虚构的界面是否合法
            cout<<"impossible\n";
            exit(0);
        }
        for(auto v:pre[u]){//如果合法,那么每个未知孩子的值就是其对应虚构的界面的值
            p[v]=ss[v];
        }
    }
    for(auto v:g[u]){//递归求解
        dfs2(v);
    }
}

最后输出所有 pip_i 即可。

Game of Gnomes

题意

nn 个人,分成 mm 组,每个人每回合可以造成人数点伤害。对手每回合可以从某组中杀死 kk 个人,如果组内人数不满 kk , 那么就把本组杀光。问如何布阵能够造成的总伤害最高。

分析

根据鸽笼原理,如果人数大于 m(k1)m*(k-1) 的话,那么至少有一个人数大于 kk 。智力正常的对手一定会先杀那 kk 个人。对于 n>m(k1)n>m*(k-1) 的情况,可以直接暴力不行砍 kk 直到范围足够小。

那么接下来考虑 nm(k1)n\leq m*(k-1) 的情况。方便起见,不妨先假设其中每组人数都不到 kk

不难发现,对方总是从大往小砍,总共砍 mm 刀。如果你人分布的不均匀的话,因为砍的总刀数一定,所以每次剩下的人都比均匀放置要少,所以在这种情况下一定是均匀放置最优。即,nn 人分 mm 组,那么其中 n%mn\%m 组放 ceil(n/m)ceil(n/m) 人,mn%mm-n\%m 组放 floor(n/m)floor(n/m) 人。

这样就是先砍若干刀 kk , 再砍若干刀 ceil(n/m)ceil(n/m),再砍若干刀 floor(n/m)floor(n/m)。造成伤害总和是一个变差的等差数列,可以 O(1)O(1) 求和。

现在我们去除每组人数不过 kk 的假设,那么不难发现,如果某组人数大于 kk ,那么对手一定会先砍 kk 个。这里有点献祭的意思,因为在对手砍这 kk 个人的时候,其实是增加了攻击轮次,这样后面所有人多打一轮,可能会产生更高的伤害。

本题中因为 m107m\leq10^7,直接暴力枚举献祭几个 kk 即可,剩下的人成为新的 nn 套用上述公式。

思考

其实这个模型有点像有着单峰性质。因为如果直接不献祭任何一组 kk 那么攻击轮次少,多打一轮可能多造成很多伤害;如果献祭很多组kk , 那么就算轮数变多,后面伤害却会变少,得不偿失。所以总伤害关于献祭组数很可能是一个单峰函数,可以用三分法求解。但是因为其数据是离散的,可能不一定满足严格的单峰性质,可以考虑三分+峰顶附件小规模遍历。这样复杂度可以从 O(m)O(m)降到 O(logm)O(log m)

https://nanti.jisuanke.com/t/45303
https://nanti.jisuanke.com/t/45305