背单词11

这题乍一看是排序贪心,然后使用领项交换来做题
由于有了第一条规则的存在,因为 n ∗ n n*n nn远大于另外两条规则所产生的代价,所以我们不会让后缀排在后面
于是乎,我们倒序建立trie树并且重构树(具体可见洛谷题解),那么问题就转换为
给这棵树标号,要求必须标了父亲才能标儿子,令每一条边的代价为儿子的序号减去父亲的序号,要求所有边代价和最少
如果按照领项交换做题,就可以得出一个方案:每次标记时,优先标记直接儿子数最少的节点
因为对一个有 n n n个儿子的节点,在最终的序列中把他往后面移动一位(注意移动后也要满足父亲在儿子的前面,所以后面一个节点不可能是这个被移动节点的儿子),答案会减去 n − 1 n-1 n1(注意这个 − 1 -1 1是因为这个被移动节点到其父亲的距离要加 1 1 1),那么后面那个节点被移动到了前一位也相同计算,可得出上述方案
但这种方法是错的,比如对于数据

7
a
ba
cba
dba
e
ge
fe

如果按照这种方法排序,得出来的序列为

a
e
fe
ge
ba
cba
dba

但实际上正确序列应该为

e
fe
ge
a
ba
cba
dba

究其原因,是因为我们刚刚的证明只能说明对于最优的排序,一定不会存在相邻的非父子关系的两项,前一项的直接儿子数更多(不信验证一下,上述两个排列都满足这个性质)。但是满足这个性质的不一定是最优排序
那为啥国王游戏那一道题目可以?因为没有“非父子关系”的限制,从而有了传递性
这题由于多了一个限制(父亲必须在儿子前面),我们就要类比“给树染色”这道题目
首先考虑所有点没有合并的时候,这就是我们上面所讨论的情况,可以知道,这个时候要选择直接儿子数最少的点
假设我们已经合并了,然后考虑相邻的两个大节点,通过列式子发现,我们要尽量将 1 − s o n [ i ] s i z e [ i ] \frac{1-son[i]}{size[i]} size[i]1son[i]大的点往前面排(也就是说这个大节点一定会在其父节点被染色之后立马被染色),其中 s o n [ i ] son[i] son[i]是这个大节点的直接儿子数(指不在这个大节点里面且是这个大节点中某个节点的儿子的节点的个数), s i z e [ i ] size[i] size[i]是这个大节点当前包含的节点数
然后我们利用类似的代码就可以写了
但是讲一下优先队列的拓展懒惰删除法。由于优先队列没有办法删除某个数,我们一般利用懒惰删除法删除这个数,但是这里优先队列放的是结构体,我们要将某一个点所代表的所有结构体都删除掉,这个时候我们用一个 m a r k mark mark数组标记节点的最新更新值,只有当优先队列里面的元素的更新值等于最新更新至,才进行拓展,具体看以下代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+10;
int trie[N*5][30];
int tot=1,n;
int flag[N*5];
ll ans;
int fa[N],f[N];
vector<int> G[N];
char s[N*5];
struct Node
{
	int a,b,last,mark,fa;
	//a表示son[i]
	//b表示size[i]
	//last表示当前大节点确定的染色顺序中最后一个被染色的节点
	//mark表示当前大节点的更新值
	//fa表示当前大节点的代表元素
	bool operator<(const Node &x) const
	{
		return (ll)(1-x.a)*b>(ll)(1-a)*x.b;
	}
}w[N];
priority_queue<Node> q;
void insert(char *str,int k)
{
	int len=strlen(str),p=1;
	for(int i=len-1;i>=0;i--)
	{
		if(!trie[p][str[i]-'a']) trie[p][str[i]-'a']=++tot;
		p=trie[p][str[i]-'a'];
	}
	flag[p]=k;
}
int getfa(int x)
{
	return f[x]==x?x:f[x]=getfa(f[x]);
}
int mark[N];//mark表示某个节点的最新更新值
int nxt[N],id[N];//nxt表示某个节点被染色之后下一个被染色的节点
//id表示这个节点被染色的序号
void dfs(int p,int k)
{
	if(flag[p]) 
	{
		G[k].push_back(flag[p]);
		fa[flag[p]]=k;
	}
	for(int i=0;i<26;i++)
	if(trie[p][i]) {
		if(flag[p]) dfs(trie[p][i],flag[p]);
		else dfs(trie[p][i],k);
	}
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s);
		insert(s,i);
	}
	dfs(1,0);
	for(int i=1;i<=n;i++)
	{
		w[i].a=G[i].size(),w[i].b=1;
		w[i].fa=i,w[i].last=i;
		w[i].mark=mark[i]=1;
		q.push(w[i]);
		f[i]=i;
	}
	w[0].a=G[0].size(),w[0].b=1;
	w[0].fa=0,w[0].last=0;
	w[0].mark=mark[0]=1;
	q.push(w[0]);
	for(int i=1;i<=n;i++)
	{
		Node temp=q.top();
		q.pop();
		if(temp.mark!=mark[temp.fa]) continue;//只有最新更新值才更新
		int fy=getfa(fa[temp.fa]);
		nxt[w[fy].last]=temp.fa;
		w[fy].last=w[temp.fa].last;
		w[fy].a=w[fy].a-1+w[temp.fa].a;
		w[fy].b+=w[temp.fa].b;
		mark[fy]++;
		w[fy].mark=mark[fy];
		f[temp.fa]=fy;//想一下以上的推导是为什么
		q.push(w[fy]);
	}
	id[0]=1;
	for(int i=nxt[0],cnt=2;i;i=nxt[i])
	id[i]=cnt++;//按照排列顺序标号
	for(int i=1;i<=n;i++)
	ans+=id[i]-id[fa[i]];//统计答案
	printf("%lld",ans);
	return 0;
}

这道题目还可以用另一种树上贪心
那么这个模型可以背下来,以下是解法
首先我们感性理解一个性质,就是对一个树,有一个根节点,他有若干颗子树,一定会在标记完了一整颗子树后才会去标记另一颗子树
因为边的代价只与父子标号之差有关,所以每个父子挨得越近越好,就不要中间插其他的了
那么洛谷最高赞题解对这个的证明是这样的:假设已经是标记完一整颗子树后再去标记另一颗子树,如果将在前面标记的子树的一个叶子节点放在后面标记的一颗子树的根的后面一位,那么这个叶子节点到其父亲的距离增加,而且这个根节点的孩子到这个根节点的距离增加,所以总距离增加,所以答案会变差。但是感觉证明也不是很严谨
实际上,这个结论的严谨证明可以利用“给树染色”这道题目的方法证明
那么有了这个性质,我们不妨每颗子树都已经标记完毕了,在考虑如何对各个子树排序
这就是一个简单的排队打水问题,规模越小的子树越靠前即可
code:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e5+10;
int trie[N*5][30];
int tot=1,n;
int flag[N*5];
ll ans;
int pos[N],fa[N],sz[N];
vector<int> G[N];
char s[N*5];
bool cmp(int a,int b)
{
	return sz[a]<sz[b];
}
void insert(char *str,int k)
{
	int len=strlen(str),p=1;
	for(int i=len-1;i>=0;i--)
	{
		if(!trie[p][str[i]-'a']) trie[p][str[i]-'a']=++tot;
		p=trie[p][str[i]-'a'];
	}
	flag[p]=k;
}
void dfs(int p,int k)
{
	if(flag[p]) 
	{
		G[k].push_back(flag[p]);
		fa[flag[p]]=k;
	}
	for(int i=0;i<26;i++)
	if(trie[p][i]) {
		if(flag[p]) dfs(trie[p][i],flag[p]);
		else dfs(trie[p][i],k);
	}
}
void dp(int p)
{
	sz[p]=1;
	int len=G[p].size();
	for(int i=0;i<len;i++)
	{
		dp(G[p][i]);
		sz[p]+=sz[G[p][i]];
	}
	sort(G[p].begin(),G[p].end(),cmp);
}
void solve(int p,int cnt)
{
	pos[p]=cnt;
	ans+=cnt-pos[fa[p]];
	int len=G[p].size(),num=1;
	for(int i=0;i<len;i++)
	{
		solve(G[p][i],cnt+num);
		num+=sz[G[p][i]];
	}
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",s);
		insert(s,i);
	}
	dfs(1,0);
	dp(0);
	solve(0,0);
	printf("%lld",ans);
    return 0;
}
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值