HDU

来源:互联网 发布:html登录界面源码 编辑:程序博客网 时间:2024/06/05 19:29

题意

这道题描述的是一个有向图,每个节点有一个权值,然后这个节点连向下一个节点(i*i+1)%n,n是这个图的节点数。那么总共有n条边n个点,肯定有环,然后题目要求从某个点出发走出来的最大字典序的路径。

分析

训练时候想到的是直接把所有最大权值的点压入队列做一次bfs来找那个路径,中途可以做剪枝操作。然后怕的是全部点都是或者大部分点都是一样的权值的,那么剪枝操作基本就不能生效,那么最后总的复杂度会达到n*n,时间复杂度不行。但是经过测试比较sqrt(n)次取最大的路径来走也能得出正解,推测是因为(i*i+1)%n的缘故导致总的链最长不会超过sqrt(n)那么长。所以并不需要比较n次。

正解

正解是树状的后缀数组的求法。因为这道题本身要找那个最大字典序的路径,会有很多段公共的后缀跟其他路径,当时训练时候也是在想后缀数组能不能运用到树上。还真的可以。思想是,本来的后缀数组中,倍增法比较i和j这个两个位置的时候,先比较出长度为1的rank,然后双关键字比较{rank[i],rank[i+k]}和{rank[i],rank[i+k]}这两个的,从1开始每次倍增2。这时候因为他不是线性的了,就要我们先预处理出这个节点一次会跳去哪里,两次会跳去哪里,四次会跳去哪里,就很像求最近公共祖先要预处理的那个far数组。那么预处理出来这个,接下来要改的部分就是双关键字比较的那个部分了。

#include<bits/stdc++.h>#define next nnn#define rank rrrusing namespace std;const int MAXN = 150010;const int MAXM = 20;int next[MAXN][MAXM];int n,m;void get_next() {//预处理出至少要跳多少次    m=ceil(log2(n));    for(int i=0; i<n; i++)        next[i][0]=((long long)i*i+1)%n;    for(int k=1; k<=m; k++) {        for(int i=0; i<n; i++) {            next[i][k]=next[next[i][k-1]][k-1];        }    }}int rank[MAXN],rank2[MAXN];int sa[MAXN];int tmp[MAXN];int k;char s[MAXN];bool cmp_sa(int i,int j) { //按字典序小的排在前面    if(rank[i]!=rank[j]) return rank[i]<rank[j];    int ri=rank[next[i][k]];    int rj=rank[next[j][k]];    return ri<rj;}int get_rank2(int i) {    return rank[next[i][k]];}queue<int> bucket[MAXN];//桶void sort(int *sa,int len,int num) { //基数排序    for(int i=0; i<len; i++)//先对第二关键字排序        rank2[sa[i]]=get_rank2(sa[i]);    for(int i=0; i<len; i++)        bucket[rank2[sa[i]]].push(sa[i]);    int i=0;    for(int j=0; j<num; j++) {        while(bucket[j].size())            sa[i++]=bucket[j].front(),bucket[j].pop();    }    for(int i=0; i<len; i++)//在对第一关键字排序        bucket[rank[sa[i]]].push(sa[i]);    i=0;    for(int j=0; j<num; j++) {        while(bucket[j].size())            sa[i++]=bucket[j].front(),bucket[j].pop();    }}void construct_sa(const char *s,int num) {    int len=strlen(s);    for(int i=0; i<len; i++)        rank[i]=9-(s[i]-'0'),sa[i]=i;//这里稍作变换s,将字典序原本反转一下    for(k=0; k<=m; k++) {        sort(sa,len,num);        num=1;//num用于记录有多少个不同的rank,来决定开多少个桶来基数排序        tmp[sa[0]]=0;//按字典序小的排在前面        for(int i=1; i<len; i++)            tmp[sa[i]]=tmp[sa[i-1]]+cmp_sa(sa[i-1],sa[i]),num=tmp[sa[i]]+1;        memcpy(rank,tmp,sizeof(int)*len);        if(num==len) break;//如果两两之间能够区分开来说明已经可以停止了    }}int main() {    if(fopen("in.txt","r"))        freopen("in.txt","r",stdin);    int t,icase=0;    scanf("%d",&t);    while(t--) {        printf("Case #%d: ",++icase);        scanf("%d",&n);        scanf("%s",s);        get_next();        construct_sa(s,10);        for(int i=0,u=sa[0]; i<n; i++) {            printf("%c",s[u]);            u=next[u][0];        }        puts("");    }    return 0;}

坑点

  1. 双关键字用sort排序会超时,要自己写一个基数排序。
  2. 倍增法的基数排序过程中有三个优化
    每次记录当前区分已经区分出来的排名num,那么每次的桶开num个就够了。
    如果已经两两区分开来了就可以提前break掉
    桶可以是用计数的方法来记录,而不是直接记谁放在了那个桶里面,但是要用到辅助的count数组。那个写法我觉得有点过于抽象了,很难记忆,就写了个队列,时间上比count数组慢了一倍。因为动态申请释放内存。
  3. 如果第二关键字排序用sa数组来推得的话,会导致数组越位,具体是因为这道题的特殊性质,比较次数并不是原本线性那样子上限是floor(log2(len)),而是ceil(log2(len)
原创粉丝点击