背单词
这题乍一看是排序贪心,然后使用领项交换来做题
由于有了第一条规则的存在,因为
于是乎,我们倒序建立trie树并且重构树(具体可见洛谷题解),那么问题就转换为
给这棵树标号,要求必须标了父亲才能标儿子,令每一条边的代价为儿子的序号减去父亲的序号,要求所有边代价和最少
如果按照领项交换做题,就可以得出一个方案:每次标记时,优先标记直接儿子数最少的节点
因为对一个有
但这种方法是错的,比如对于数据
7
a
ba
cba
dba
e
ge
fe
如果按照这种方法排序,得出来的序列为
a
e
fe
ge
ba
cba
dba
但实际上正确序列应该为
e
fe
ge
a
ba
cba
dba
究其原因,是因为我们刚刚的证明只能说明对于最优的排序,一定不会存在相邻的非父子关系的两项,前一项的直接儿子数更多(不信验证一下,上述两个排列都满足这个性质)。但是满足这个性质的不一定是最优排序
那为啥国王游戏那一道题目可以?因为没有“非父子关系”的限制,从而有了传递性
这题由于多了一个限制(父亲必须在儿子前面),我们就要类比“给树染色”这道题目
首先考虑所有点没有合并的时候,这就是我们上面所讨论的情况,可以知道,这个时候要选择直接儿子数最少的点
假设我们已经合并了,然后考虑相邻的两个大节点,通过列式子发现,我们要尽量将
然后我们利用类似的代码就可以写了
但是讲一下优先队列的拓展懒惰删除法。由于优先队列没有办法删除某个数,我们一般利用懒惰删除法删除这个数,但是这里优先队列放的是结构体,我们要将某一个点所代表的所有结构体都删除掉,这个时候我们用一个
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构