• CSP2019 题解


    D1T1

    显然把所有排列列出来,不难发现答案的第 (i) 位只与 (k) 二进制拆分后的 (i) 位是 0/1 和 (i-1) 位是 0/1 有关。

    那么把 (k) 分解,记分解后第 (i) 位为 (a_i),那么答案第 (i) 位为 a[i-1]?!a[i]:a[i]

    D1T2

    先考虑链如何做,是个经典问题,记 (f_u) 表示 ([1,u]) 中的新增的匹配数量。

    显然 (f_u) 可以从上一次匹配的左括号位置转移,求答案就记个前缀和就好了。

    考虑如何扩展到树上,记 (f_u) 表示根到 (u) 这条路径上新增的匹配数量,(dp_u) 表示根到 (u) 的匹配数量。

    那么 (dp_u=dp_{fa_u}+f_u)

    我们维护一个栈,每次搜到一个左括号就把他压到栈里,(f_u=0)

    当搜到右括号时,如果栈是空的,那么 (f_u=0)

    否则,(f_u=f_{fa_{stk_{tp}}}+1),这个转移的意思就是到 (u) 的新增匹配,要么是由栈顶父亲那里的匹配加上 ([stk_{tp},u]) 这一对括号构成的,要么是 ([stk_{tp},u]) 这对括号。

    注意到回溯的时候栈里存的可能不是 ([1,u]) 里已经匹配的,那么我们每次退栈的时候记录退栈的元素,搜完子树后再入栈。

    代码

    D1T3

    参考资料:1 2 3 4 5 6

    先考虑菊花图如何处理。

    画画图不难发现数字的移动形成一个环,那么要让字典序最小,每个数字我们贪心的选择能选择的最小数字,并且不能组成小环,这个不难用并查集维护。

    再考虑链如何处理。

    考虑一个数字要从 (u_1) 移动到 (u_k),那么路径 ([u_1,u_k]) 上:

    • 对于起点 (u_1),边 ((u_1,u_2)) 一定是 (u_1) 出边里最早删的。

    • 对于中间点 (u_i),入边 ((u_{i-1},u_i)) 要比出边 ((u_i,u_{i+1})) 早删。

    • 对于终点 (u_k),入边 ((u_{k-1},u_k)) 一定是 (u_k) 最后删的边。

    那么可以对每个点记个 tag,0 表示没有限制,1 表示入边大于出边,2 表示入边小于出边,仍然顺序枚举数字,检查每个数字从初始位置向左向右能走到的点中的最小编号。

    处理完两种特殊情况,再来处理一般情况。

    链的特殊情况启发了我们,同一个节点相邻的边才会有先删后删的限制。

    那么对于一般情况,一句话来说就是把菊花图做法和链的做法结合一下。

    具体的,我们维护一个点删边的顺序集合,记 (fir_u) 表示最先删的边,(lst_u) 表示最后删的边,(pre_i) 表示是否有前驱边,(suf_i) 表示是否有后继边。

    重新考虑一下一个数字如何从一个点移动到另一个点。

    • 起点

      • 它连出的边必须在删边序列中的第一个。

      • 如果我们通过这条边确认了删边序列的第一条边,若此时删边序列的最后一条边已确定且与删边序列的第一条边同处于一个确定的删边序列中,那么若有边没加入删边序列,那么它无法加入删边序列。

    • 中间点

      • 连入这个点的边和连出这个点的边必须在删边序列中是相邻的。

      • 如果我们通过这两条边将第一条删除的边和最后一条删除的边连在一起,若此时还有边没加入,那么它无法加入删边序列。

    • 终点

      • 它连入的边必须在删边序列中的最后一个。

      • 如果我们通过这条边确认了删边序列的最后一条边,若此时删边序列的第一条边已确定且与删边序列的最后一条边同处于一个确定的删边序列中,那么若有边没加入这个删边序列,那么它无法加入删边序列。

    int dfs(reg int u,reg int f)
    {
    	int ret=0x3f3f3f3f;
    	if(f&&(!t[u].lst||t[u].lst==f)) // 终点
    	{
    		if(!t[u].suf[f]&&!(t[u].fir&&deg[u]>1&&t[u].find(f)==t[u].find(t[u].fir))) ret=u;
    	}
    	for(reg int i=head[u];~i;i=nxt[i])
    	{
    		int v=pnt[i],id=i>>1;
    		if(f==id) continue;
    		if(!f) // 起点
    		{
    			if(!t[u].fir||t[u].fir==id)
    			{
    				if(t[u].pre[id]) continue;
    				if(t[u].lst&&deg[u]>1&&t[u].find(t[u].lst)==t[u].find(id)) continue;
    				ret=dmin(ret,dfs(v,id));
    			}
    			else continue;
    		}
    		else // 中间点
    		{
    			if(t[u].fir==id||t[u].lst==f||t[u].find(id)==t[u].find(f)) continue;
    			if(t[u].pre[id]||t[u].suf[f]) continue;
    			if(t[u].fir&&t[u].lst&&deg[u]>2&&t[u].find(t[u].fir)==t[u].find(f)&&t[u].find(t[u].lst)==t[u].find(id)) continue;
    			ret=dmin(ret,dfs(v,id));
    		}
    	}
    	return ret;
    }
    

    找到终点后把它删掉就好了。

    代码

    D2T1

    考虑到每种主要食材至多在一半的菜中被使用这个条件比较棘手,直接设计状态要考虑到每种食材的使用次数,不如正难则反,求出不合法的方案数,用总方案数减掉就是答案。

    (g_{i,j}),表示前 (i) 种烹饪方式,每种最多选一道菜,已经选了 (j) 道菜的方案数。

    转移分选或者不选转移:(g_{i,j}=g_{i-1,j}+g_{i-1,j-1} imes sum_i),其中 (sum_i) 表示第 (i) 种方式可做的菜数量。

    总方案为 (displaystyle sum_{i=1}^{n} g_{n,i})

    不合法的方案只要有一种食材的数量超过菜的数量的一半,那么考虑枚举哪些菜超过了。

    (f_{i,j,k}) 表示前 (i) 种方式,要超限的食材出现次数为 (j),其他食材出现次数和为 (k) 的方案数。

    转移也是分不选,选超限,不选超限转移:(f_{i,j,k}=f_{i-1,j,k}+f_{i-1,j-1,k} imes a_{i,t}+f_{i-1,j,k-1} imes (sum_i-a_{i,t})),其中 (t) 为超限食材编号。

    至此复杂度为 (mathcal O(n^3m)),无法通过,考虑优化。

    注意到不合法方案数是 (displaystyle sum_{k<j} f_{n,j,k}),把条件移项,得到 (j-k>0),那么是否是不合法方案只要看 (j-k) 的符号,所以我们可以把二三两维合并成一维,记录 (j-k) 的值。

    因为 (j-k) 的值可能小于 (0),所以要对下标加上一个大数来处理。

    代码

    D2T2

    首先不难发现一个 dp :记 (dp_{i,j}) 表示划分了 (i) 个数,上一次划分位置是 (j) 的最小值。

    记个前缀和转移,(dp_{i,j}=displaystyle min_{sum_i-sum_k leq sum_j-sum_i} {dp_{k,i}+(sum_j-sum_i)^2}),拿到 36 分。

    考虑优化,固定 (i),发现 (j) 变化的时候, (k) 也在随着变化。

    那么维护一下 (dp_{k,i}) 的最小值就好了,拿到 64 分。

    继续优化,考虑这么一个式子 ((a+b)^2 geq a^2+b^2)

    从这个式子我们得知尽量多分几段可以使答案更小。

    所以我们每次只要找到第一个能转移的 (k) 进行转移就行了,记转移点为 (f_i)

    那么我们要求的就是最大的 (k) 使得 (sum_i-sum_k geq sum_k-sum_{f_k})

    移项一下,得到 (sum_i geq 2 imes sum_k-sum_{f_k})

    可以通过暴力打出决策点,发现 (f_i) 是单调的,即假如有 (t<f_i),那么 (t) 绝对不是 (f_{i+1})

    所以这个用单调队列维护一下即可。

    不想写高精度所以用了 __int128。

    代码

    D2T3

    参考资料:1

    40 分就是枚举哪条边被删了,然后暴力求重心,统计答案。

    链的特殊情况就拍平成序列,重心一定是在链的中点,分奇偶讨论。

    完美二叉树情况不太会,听说找规律。

    考虑到暴力的做法找重心统计答案过程很不优秀,那么我们从重心的性质里找到突破口:树的重心一定在根节点的重链上。

    证明可以考虑反证法,证明过程在参考资料里。

    有了这个性质我们就可以考虑倍增,记 (f_{u,i}) 表示从 (u) 沿重链向下跳 (2^i) 步能到哪里,记 (t) 为当前根节点,(sz_i) 表示 (i) 子树大小。

    那么我们要一直跳到 (sz{f_{u,i}}leq dfrac{sz_t}{2}),就能找到可能是重心的点,然后判断它和它的重儿子是不是重心,统计答案。

    总结一下,首先预处理子树大小,重儿子和次重儿子,然后随便找个点当作根节点,dfs 下去,枚举连向儿子的边,求出从这断开后重儿子是哪个点和子树大小,倍增统计答案,回溯的时候把重儿子和子树大小还原,具体可以看代码。

    代码

  • 相关阅读:
    数据处理之PostgreSQL过程语言学习
    Thinkphp中的assign() 和 display()
    JS截取与分割字符串常用技巧总结
    三种JS截取字符串方法
    十大经典排序算法的JS版
    js时间与毫秒互相转换
    javascript--清除表单缓存
    JS join()和split()方法、reverse() 方法、sort()方法
    JS数组去重的几种常见方法
    CSS样式大全
  • 原文地址:https://www.cnblogs.com/Lonely-233/p/15178089.html
Copyright © 2020-2023  润新知