UOJ Round #7 部分题解

A

可以想到用阶乘进位制来将 m m m 拆分掉,但是很可能导致某一位的系数乘权值不是 n ! n! n! 的系数,题解采用的方法是用反过来的阶乘进位制,即第 i i i 位的权值是 n × ( n − 1 ) × . . . × ( i + 1 ) n\times (n-1)\times ... \times (i+1) n×(n1)×...×(i+1),并且这一位满 i i i 1 1 1,这样可以保证第 i i i 位的系数小于 i i i,那么系数乘权值就一定是 n ! n! n! 的约数了。

代码如下:

#include <cstdio>
#define ll long long

int n;
ll m,now,prod=1;

int main()
{
	scanf("%d %lld",&n,&m);now=n;
	while(m>=now&&now>1)
	{
		if(m%now)printf("%lld\n",m%now*prod);
		m/=now;prod*=now;now--;
	}
	printf("%lld",m*prod);
}

C

一开始想了个大贪心,每次遍历完当前点除了最大的子树外的所有子树后往最大的子树走一步。

后来发现,到遍历其它子树的最后一步时,本体和分身可以同时往下走一步,这样能节省一丢丢时间。

然后发现我被样例无情的干掉了,膜完题解后,我知道,我还是太naive了。

算法1

题解中提到了一个劣质筷子模型,指的是:如果树像两根筷子,它们吃饭的那一端接在了一起,那么答案就是一根筷子的长度,但是如果这筷子是劣质的一次性筷子,那么旁边就会有一些毛刺,假如这些毛刺都靠近吃饭的一端,那么显然最优方案就是先走完毛刺,然后再本体分身一起移动走完两根筷子,那么答案就是一根筷子的长度再多一点。

这个模型就把我那个naive的做法锤烂了……于是先考虑暴力些的做法:

首先我们知道,对于一棵树,假如本体站在树根,让分身去遍历完子树,这样的代价其实是所有叶子到根的距离之和,因为我们将叶子遍历完了,非叶子节点也肯定遍历完了。

考虑暴力 O ( n 2 ) O(n^2) O(n2) d p dp dp,对于当前点,枚举子树中的一个点作为终点,然后将 当前点子树内 终点子树外 的子树遍历完,留一个最深的叶子不遍历,然后在分身去最深叶子的同时,本体从当前点走到终点即可。

f [ x ] f[x] f[x] 表示根节点走到 x x x (并且遍历完了 x x x 子树外的所有点)的最少步数, d e e p [ x ] deep[x] deep[x] 表示 x x x 的深度, d m [ x , y ] dm[x,y] dm[x,y] 表示 x x x y y y 这一段节点的子树(不包括 y y y 的子树)内的最深节点深度, d s u m [ x ] dsum[x] dsum[x] 表示 x x x 子树内的所有叶子节点的深度之和, s i z e [ x ] size[x] size[x] 表示 x x x 的子树内的叶子节点数,方程长这样:
f [ e d ] = min ⁡ { f [ s t ] + d s u m [ s t ] − d s u m [ e d ] − ( s i z e [ s t ] − s i z e [ e d ] ) × d e e p [ s t ] + max ⁡ ( d e e p [ e d ] − d m [ s t , e d ] , 0 ) } f[ed]=\min\{f[st]+dsum[st]-dsum[ed]-(size[st]-size[ed])\times deep[st]+\max(deep[ed]-dm[st,ed],0)\} f[ed]=min{f[st]+dsum[st]dsum[ed](size[st]size[ed])×deep[st]+max(deep[ed]dm[st,ed],0)}

其中, d s u m [ s t ] − d s u m [ e d ] − ( s i z e [ s t ] − s i z e [ e d ] ) × d e e p [ s t ] dsum[st]-dsum[ed]-(size[st]-size[ed])\times deep[st] dsum[st]dsum[ed](size[st]size[ed])×deep[st] 这一段是在处理 s t st st e d ed ed 这一段新增的所有叶子节点到 s t st st 的距离之和, max ⁡ ( d e e p [ e d ] − d m [ s t , e d ] , 0 ) \max(deep[ed]-dm[st,ed],0) max(deep[ed]dm[st,ed],0) 则是看最深节点和 e d ed ed 谁到 s t st st 距离更长。

算法2

d m [ x ] dm[x] dm[x] 表示 x x x 的最深子树的深度(除去 e d ed ed 这棵子树)。

对于一个点 e d ed ed,无非就是两种更新方式:

  1. 假如父亲只有我一个儿子,那么可以由 f [ 父 亲 ] + 1 f[父亲]+1 f[]+1 得到
  2. 找到一个满足 d m [ s t ] ≥ d e e p [ e d ] dm[st]\geq deep[ed] dm[st]deep[ed] s t st st,由他转移过来

可以发现,如果第二种方式中找不到这样的 s t st st,那么这个 e d ed ed 的最优解一定可以由方式 1 1 1 得到。如果找得到的话,两种方式取个最大值即可。

我们注意到, s t st st e d ed ed 这段路中,最后遍历最深节点时,本体和分身肯定会在 d m dm dm 最大的节点那里分开,仔细想想,对于当前 e d ed ed 而言,以那个分开的节点作为 s t st st 显然不会更差,因为相对于原来的 s t st st,一起走的部分还是一起走了,而叶子节点到 s t st st 的距离和又减少了。

于是,我们可以考虑只用 d m dm dm 比下面的都要大的 s t st st 来转移,这个维护一个单调栈即可,由于一个点子树内至少有 d m [ x ] − d e e p [ x ] dm[x]-deep[x] dm[x]deep[x] 个点,所以这个栈的长度是 n \sqrt n n 的,每次枚举这个栈中的元素,假如满足方式 2 2 2 的条件,那么用它更新一下 e d ed ed,时间复杂度 O ( n n ) O(n\sqrt n) O(nn )

算法3

接下来你只需要注意到,我们不需要让栈中每一个满足条件的 s t st st 都更新一遍 e d ed ed,只需要用满足条件且深度最大的那个即可。原因和上面一样:一起走的部分还是一起走了,而叶子节点到st的距离和又减少了。

我们不妨对这颗树进行一种链剖分,将一个节点连向自己的最深子树的边染成黑色,那么发现,每次走黑边时,就要往栈里面丢次深子树的深度,否则就丢最深子树的深度。

假如丢的是最深子树的深度,那么这个栈就要pop成刚来到这条黑链时的样子,因为这个最深子树深度比这条黑链上面丢进来的次深子树深度都要深。

可以发现,由于push和pop的次数都是 n n n,所以维护这个栈的时间是 O ( n ) O(n) O(n) 的。

最后

扯一扯代码实现的细节。

由于这题常数太小了,所以算法3中找深度最大的那个节点的部分,在代码中其实是暴力实现的,就是每次从栈顶暴力往下找。(其实是因为蒟蒻不知道怎么优化qwq)

以及,丢最深子树深度时,代码中甚至直接不pop,直接丢进去,因为显然如果这个最深的都不能成为 s t st st,那么应该pop掉的次深就更不可能了。而不pop掉他们,实现会方便很多(虽然会变慢一些)。

以及,由于我们找到的 s t st st 都满足 d m [ s t ] ≥ d e e p [ e d ] dm[st]\geq deep[ed] dm[st]deep[ed],所以 d p dp dp 方程中最后的 max ⁡ \max max 就不用要了,因为肯定是 0 0 0

代码如下:

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 5000010
#define ll long long
#define FA fa[now]
#define inf 999999999999999999

int n,fa[maxn]; char s[maxn<<1];
int deep[maxn],dson[maxn],dm[maxn],dm2[maxn],size[maxn];
//dm记录最深子树深度,dm2记录次深,dson表示黑链上的儿子,也就是子树最深的那个儿子
int now,tot;
ll dsum[maxn],f[maxn],ans=inf;
struct par{int x,y;}zhan[maxn]; int t=0;

int main()
{
	scanf("%d %s",&n,s);
	if(n==1)return printf("0"),0;
	now=tot=1;deep[1]=1;
	for(int i=1;i<2*n-1;i++)
	if(s[i]=='(')
	{
		fa[++tot]=now;
		now=tot; deep[now]=deep[FA]+1;
	}
	else
	{
		if(!dson[now])dsum[now]=dm[now]=deep[now],size[now]=1;//假如是叶子节点
		if(dm[FA]<dm[now])dm2[FA]=dm[FA],dm[FA]=dm[now],dson[FA]=now;
		else if(dm2[FA]<dm[now])dm2[FA]=dm[now];
		dsum[FA]+=dsum[now];size[FA]+=size[now];
		now=FA;
	}
	now=tot=1;
	for(int i=1;i<2*n-1;i++)
	if(s[i]=='(')
	{
		int next=++tot;
		if(next==dson[now])//如果走黑边
		{
			while(t>0&&dm2[now]>zhan[t].y)t--;//将dm2丢进去,该pop的pop
			if(dm2[now]>0)zhan[++t]=(par){now,dm2[now]};
		}
		else zhan[++t]=(par){now,dm[now]};//否则干脆不pop,直接丢进去
		now=next; int p=t;
		while(p>0&&zhan[p].y<deep[now])p--;//暴力找到最深的满足要求的st
		if(dm2[FA]==0)f[now]=f[FA]+1; else f[now]=inf;
		if(p)
		{
			int x=zhan[p].x;
			f[now]=min(f[now],f[x]+dsum[x]-dsum[now]-((ll)size[x]-size[now])*deep[x]);
		}
	}
	else
	{
		if(!dson[now]&&f[now]<ans)ans=f[now];
		now=FA; if(zhan[t].x==now)t--;
	}
	printf("%lld",ans);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值