[SCOI2016]背单词-题解

题意的话就看题面吧。

我们一步一步的来分析:

首先吃最少的泡椒,那么显然可以贪心,由于 n × n n\times n n×n贡献的肯定比后面的方式都大,所以我们考虑将一个串它的所有存在的后缀串全部先放在前面,这时就不会用第一种了,然后我们考虑,可以将这种关系用边连起来,就成了一棵树,我们可以举个例子来看:

5
a
b
ba
bb
bba

这个例子便可以连出这样的一张图:

lz

我们发现,如果先放 a a a这边的,那么最后就会是:

a - 1
ba - 1
bba - 1
b - 4
bb - 1

cost = 8

但是我们明显可以发现如果先放 b b b这边的,那么当到 a a a时,它只会有 3 3 3的代价,而其他都是 1 1 1的代价,所以如下这样会更优秀:

b - 1
bb - 1
a - 3
ba - 1
bba - 1

cost = 7

那么贪心的考虑,也就是这棵树上,我们按照子树大小,从小到大的遍历会更优秀一些。

所以一种方法是用Hash算法,在 O ( n 2 ) O(n^2) O(n2)的时间内建出这个树,然后遍历一遍排个序就可以出答案了。

目前瓶颈就在于建树,我们可以发现它的每个都是和后缀有关,但是直接建一棵广义后缀自动机太麻烦了,所以我们将其翻转后,后缀就变成了前缀,那么只需插入到一棵trie(字典)树中即可,对于一个串是另一个串的前缀,那么在字典树上它就是另一个的祖先(在另一个同一条链的上面),然后遍历一遍即可建出原树。

复杂度变成 O ( ∣ l e n ∣ ∗ 26 + n ) O(|len|*26+n) O(len26+n)就可以过了。

代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define ll long long
using namespace std;
const int N=1e5+5,M=6e5+1;
int n,bef[N];
char str[M];
ll ans;
int son[M][26],f[N],flag[M],sze[N],bel[M],tot=1;
struct ss{
    int to,last;
    ss(){}
    ss(int a,int b):to(a),last(b){}
}g[M];
int head[N],cnt;
void add(int a,int b){g[++cnt]=ss(b,head[a]);head[a]=cnt;f[b]=a;}
void addin(char *s,int k){
    int id,now=1;
    int len=strlen(s);
    for(int i=len-1;i>=0;i--){
       id=s[i]-'a';
       if(!son[now][id]) son[now][id]=++tot;
       now=son[now][id];
    }
    bel[now]=k;//建trie树,记录是哪个字符串
}
struct node{
    int sze,id;
    node(){}
    node(int a,int b):sze(a),id(b){}
    bool operator <(node a)const{
		return sze<a.sze||(sze==a.sze&&id<a.id);
	}//按子树大小排序
};
vector <node> vec[N];
void dfs(int a,int fa){
    if(bel[a]) add(fa,bel[a]);
    //记录是从fa那个串来的
    for(int i=0;i<26;i++){
        if(son[a][i]) dfs(son[a][i],bel[a]?bel[a]:fa);
    }//建树
}
void find(int a){
    sze[a]=1;
    for(int i=head[a];i;i=g[i].last){
        find(g[i].to);
        vec[a].push_back(node(sze[g[i].to],g[i].to));
        sze[a]+=sze[g[i].to];
    }
    sort(vec[a].begin(),vec[a].end());//排序
}
int rak;
void get(int a){
    if(a) bef[a]=++rak;
    for(int i=0;i<vec[a].size();i++) get(vec[a][i].id);//遍历顺序,打上时间戳
}
void file(){
    freopen("word.in","r",stdin);
    freopen("word.out","w",stdout);
}
void close(){
    fclose(stdin);
    fclose(stdout);
}
int main(){
    file();
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%s",str);
        addin(str,i);
    }
    dfs(1,0);
    find(0);
    get(0);
    for(int i=1;i<=n;i++) ans+=1ll*(bef[i]-bef[f[i]]);//统计答案
    printf("%lld\n",ans);
    close();
    return 0;
}
### 关于 SCOI2009 WINDY 数的解法 #### 定义与问题描述 WINDY数是指对于任意两个相邻位置上的数字,它们之间的差至少为\(2\)。给定正整数区间\([L, R]\),计算该范围内有多少个WINDY数。 #### 动态规划方法解析 为了高效解决这个问题,可以采用动态规划的方法来处理。定义状态`dp[i][j]`表示长度为`i`且最高位是`j`的WINDY数的数量[^3]。 - **初始化** 对于单个数字的情况(即只有一位),显然每一位都可以单独构成一个合法的WINDY数,因此有: ```cpp dp[1][d] = 1; // d ∈ {0, 1,...,9} ``` - **状态转移方程** 当考虑多位数时,如果当前位选择了某个特定数值,则下一位的选择会受到限制——它必须满足与前一位相差不小于2的要求。具体来说就是当上一高位为`pre`时,当前位置可选范围取决于`pre`的具体取值: - 如果`pre >= 2`, 则可以选择`{0... pre-2}` - 否则只能从剩余的有效集合中选取 这样就可以通过遍历所有可能的状态来进行状态间的转换并累加结果。 - **边界条件处理** 特殊情况下需要注意的是,在实际应用过程中还需要考虑到给出区间的上下限约束。可以通过逐位比较的方式判断是否越界,并据此调整有效状态空间大小。 ```cpp // 计算不超过num的最大windy数数量 int calc(int num){ int f[15], g[15]; memset(f, 0, sizeof(f)); string s = to_string(num); n = s.size(); for (char c : s) { a[++len] = c - '0'; } // 初始化f数组 for (int i=0;i<=9;++i)f[1][i]=1; // DP过程省略... return sum; } long long solve(long long L,long long R){ return calc(R)-calc(L-1); } ``` 此代码片段展示了如何利用预处理好的`dp`表快速查询指定范围内的WINDY数总量。其中`solve()`函数用于返回闭区间\[L,R\]内符合条件的总数;而辅助函数`calc()`负责根据传入参数构建相应的状态序列并最终得出答案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VictoryCzt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值