算法竞赛笔记vi

字符串

KMP算法,各个书上写的KMP算法的区别:
算法竞赛书上面的next数组的定义是:
n e x t [ i ] next[i] next[i]表示"A中以i结尾的非前缀子串" 与 "A的前缀"能够匹配的最大长度,即:
n e x t [ i ] = m a x { j } next[i] = max\{j\} next[i]=max{j} 其中 j < i j<i j<i 并且 A [ i − j + 1 ∼ i ] = A [ 1 ∼ j ] A[i - j + 1\sim i] = A[1\sim j] A[ij+1i]=A[1j]

严蔚敏书上的next数组的定义是:
n e x t [ j ] = k next[j] = k next[j]=k,则 n e x t [ j ] next[j] next[j]表明当模式中第 j j j个字符与主串中相应字符"失配"时,在模式中需重新和主串中该字符进行比较的字符的位置, n e x t [ 1 ] = 0 next[1] = 0 next[1]=0表示 模式串第0的位置为一个通配符’*’,当串中一个字符next到了next[1]说明模式串没有字符可以与之对齐,那么两者的指针全部都加1

邓俊辉书上的next数组定义与严蔚敏书上的next数组定义是一样的,区别是严蔚敏书上的串是下标是从1开始,严蔚敏书上的串下标为0的位置里面放的是字符串的长度,而邓俊辉书上串的下标是从0开始

POJ 1961 Period

在这里插入图片描述
我的想法是 构造字符串的hash表,然后依次比较序列 [ 0 , 1 ) [ 1 , 2 ) , . . . , [ n − 2 , n − 1 ) [0,1) [1,2) ,...,[n-2,n-1) [0,1)[1,2),...,[n2,n1)中每个区间,比较序列 [ 0 , 2 ) [ 2 , 4 ) [ 4 , 6 ) , . . . , [ n − 3 , n − 1 ) [0,2) [2,4) [4,6) ,...,[n-3,n-1) [0,2)[2,4)[4,6),...,[n3,n1) [ 0 , n − 1 / 2 ) [ n − 1 / 2 , n − 1 ) [0,n-1/2) [n-1/2,n-1) [0,n1/2)[n1/2,n1) 时间复杂度为 O ( n 2 ) O(n^2) O(n2)
AC了 21464K 2266MS

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>

using namespace std;
const int N = 1000004;
char data[N];
unsigned long long p[N],weight[N];

unsigned long long Hash(int l,int r){
    return p[r] - p[l-1] * weight[r-l+1];
}

int main()
{
    int n,cases = 1;
    while(true)
    {
        scanf("%d",&n);
        if(n == 0)
            break;
        scanf("%s",data+1);
        map<int,int> m;
        p[0] = 0,weight[0] = 1;
        for(int i=1;i<=n;i++){//构造Hash表
            p[i] = p[i-1] * 131 + data[i];
            weight[i] = weight[i-1] * 131;
        }
        printf("Test case #%d\n",cases);
        cases++;
        for(int len = 1;len <= n/2;len++){//遍历每种可能的长度
            int ind = 1+len,k = 1,flag = 1;
            while(flag){
                flag = 0;
                if(ind + len - 1 <= n && Hash(1,len) == Hash(ind,ind+len-1)){//依次顺着比较同等长度的字符子串
                    ind = ind + len;
                    k++;
                    flag=1;
                }
                if(k >= 2 && flag)//对于k>1的结果 存入map中,map里面存着对应下标的最大K的值
                    m[ind-1] = max(m[ind-1],k);
            }
        }
        for(map<int,int>::iterator it = m.begin();it != m.end();it++)
            printf("%d %d\n",it->first,it->second);
        puts("");
    }
    return 0;
}

既然这一章的知识点是KMP,那么我就想想怎么用KMP来解这道题,想不出来,康康书上怎么写:
书上是根据一个引理来进行解题的:
S [ 1 ∼ i ] S[1\sim i] S[1i]具有长度为 l e n < i len < i len<i的循环元的充要条件是len能整除i并且 S [ l e n + 1 ∼ i ] = S [ 1 ∼ i − l e n ] S[len + 1 \sim i] = S[1 \sim i - len] S[len+1i]=S[1ilen]
5004K 157MS 这快多了

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
using namespace std;
const int N = 1000004;
char data[N];
int _next[N];
int main()
{
    int n,cases = 1;
    while(true){
        scanf("%d",&n);
        if(n == 0) break;
        scanf("%s",data+1);
        _next[1] = 0;
        for(int i=2,j=0;i <= n;i++){
            while(j > 0 && data[i] != data[j+1]) j = _next[j];
            if(data[i] == data[j+1]) j++;
            _next[i] = j;
        }
        printf("Test case #%d\n",cases);
        cases++;
        for(int i=2;i<=n;i++){
            if(_next[i] > 0 && i % (i - _next[i]) == 0)
                printf("%d %d\n",i,i/(i-_next[i]));
        }
        puts("");
    }
    return 0;
}
最小表示法

给定一个字符串 S [ 1 ∼ n ] S[1 \sim n] S[1n],如果我们不断把它的最后一个字符放到开头,最终会得到n个字符串,称这n个字符串是循环同构的。这些字符串中字典序最小的一个,称为字符串S的最小表示
求一个字符串的最小表示
方法是用双指针来遍历字符串,详细见代码:

int n = strlen(s+1);
for(int i=1;i<=n;i++) s[n+i] = s[i];
int i=1,j=2,k;
while(i <= n && j <= n){
	for(k = 0;k < n && s[i+k] == s[j+k];k++);
	if(k == n) break;
	if(s[i+k] > s[j+k]){
		i = i + k + 1;
		if(i == j) i++;
	}else{
		j = j + k + 1;
		if(i == j) j++;  
	}
}

一开始的for循环是将字符串复制一遍,这样对于新串s
这里要解释的是 1 为什么 k = n k = n k=n的时候就break了,当 k = n k = n k=n
s [ m ] s [ m + 1 ] s [ m + 2 ] s [ m + 3 ] s [ m + 4 ] . . . s [ m + n − 1 ] s[m] s[m+1] s[m+2] s[m+3] s[m+4] ... s[m+n-1] s[m]s[m+1]s[m+2]s[m+3]s[m+4]...s[m+n1] s [ l ] s [ l + 1 ] s [ l + 2 ] s [ l + 3 ] s [ l + 4 ] . . . s [ n + l − 1 ] s[l] s[l+1] s[l+2] s[l+3] s[l+4] ... s[n+l-1] s[l]s[l+1]s[l+2]s[l+3]s[l+4]...s[n+l1]相等 由上题知 s [ m ] s [ m + 1 ] . . s [ l − m − 1 ] s[m] s[m+1] .. s[l-m-1] s[m]s[m+1]..s[lm1]是一个循环元 而且在这个循环元中 这个串是这个循环元同构串中最小的,因为如果有比这个小的那么在m处的指针应该会跳转。
while里面的操作就是
如果在 i + k i+k i+k j + k j+k j+k处发现不相等,假设 s [ i + k ] > s [ j + k ] s[i+k] > s[j+k] s[i+k]>s[j+k],那么我们当然可以知道从 i i i起始的串不是 S S S的最小表示,因为存在一个更小的循环同构串从 j j j起始的循环同构串,除此之外我们还知道从 i + 1 i+1 i+1 i + 2 i+2 i+2 i + k i+k i+k起始的都不是 S S S的最小表示 这是因为对于 1 ≤ p ≤ k 1 \le p \le k 1pk,存在一个比从 i + p i+p i+p起始的循环同构串更小的循环同构串 从 j + p j+p j+p开始的循环同构串

Trie树

CH1601 前缀统计

在这里插入图片描述
感觉这是trie的模板题,用一个数组_end来记录以某个下标为终止的字符串的个数,我的代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>

using namespace std;
const int N = 1000004;
int trie[N][26];
int _end[N];
char data[N];

int main()
{
    int n,m,tot = 1;
    scanf("%d%d",&n,&m);
    while(n--){
        scanf("%s",data+1);
        int len = strlen(data+1),p = 1;
        for(int i=1;i<=len;i++){
            int ch = data[i] - 'a';//在trie的p位置 插入ch分支
            if(trie[p][ch] == 0) trie[p][ch] = ++tot;//如果trie树里面没有的话 就插入相应项tot
            p = trie[p][ch];//移动结点到子结点
        }
        _end[p]++;//以下标为p的子串的个数加1
    }
    while(m--){
        scanf("%s",data+1);
        int len = strlen(data+1),p = 1,ans = 0;
        for(int i=1;i<=len;i++){
            p = trie[p][data[i]-'a'];
            if(p == 0) break;//说明后面没有匹配的了,这个时候break就ok了
            ans += _end[p];//加上字符串终止在p位置的字符串个数
        }
        printf("%d\n",ans);
    }
    return 0;
}

在CH上 这个代码出错
在这里插入图片描述
找了半天没有找到错误原因,通过一个一个与光盘进行对照修改 发现错在数组名_end有问题的,改成ed就ok了

CH1602 The XOR Largest Pair

在这里插入图片描述
一开始想暴力算法是 将 A i A_i Ai A i + 1 . . . A N A_{i+1} ... A_{N} Ai+1...AN进行XOR运算,然后将结果的最大值更新mx变量,这样的话 时间复杂度为 O ( n 2 ) O(n^2) O(n2)
这个题不像上一题 限制了字符串总长度,将 A i A_i Ai看成是长度为31的二进制字符串

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>

using namespace std;
const int N = 3200004;
int trie[N][2];
int weight[32];

int main()
{
    int n,x,tot = 1,mx = 0;
    weight[31] = 1;//因为题目条件中 所有数都是小于2^31 所以这里只取31位二进制位
    for(int i=30;i >= 1;i--)
        weight[i] = weight[i+1] * 2;//weight[i] 为 2的31-i次方的值,倒着构造的原因是顺着遍历是因为二进制从左到右是权值递减的
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        int p = 1,w = 1,ans = 0;
        scanf("%d",&x);
        for(int k=1;k<=31;k++){//将每个输入x看成31位的二进制位
            if(x >= weight[k]){//当第k位二进制位为1时,x就大于等于weight[k]
                x -= weight[k];//把这个第k位二进制位的1给去掉
                if(i > 1 && trie[w][0] != 0) {ans += weight[k];w = trie[w][0];}
                else if(i > 1) w = trie[w][1];
                if(trie[p][1] == 0) trie[p][1] = ++tot;//构造trie树
                p = trie[p][1];
            }
            else{
                if(i > 1 && trie[w][1] != 0) {ans += weight[k];w = trie[w][1];}
                else if(i > 1) w = trie[w][0];
                if(trie[p][0] == 0) trie[p][0] = ++tot;//构造trie数
                p = trie[p][0];
            }
        }
        mx = max(ans,mx);
    }
    printf("%d\n",mx);
    return 0;
}

for循环里面if和else中根据第k位是1还是0进行对称操作,当第i个数输入的时候,在1,…,i-1个数的trie树中进行寻找最大xor值,第i个数的第一个二进制位为b那么就在查看!b分支是否存在, 因为 !b xor b为1,而且顺着找二进制位 二进制位的权值是单调递减的,那么第一个二进制位是最重要的。如果!b不存在那么b分支一定存在 因为trie树不为空,那么就走b分支,然后类似 依次处理后面的二进制位。i从2遍历到n能包含所有可能的情况

POJ 3764 The XOR Longest Path

在这里插入图片描述
这个和上一题不一样的是 这里是多个值的xor,因为是树,所以没有环,我就将树的结点x, p ( x ) p(x) p(x)作为根节点一直到结点x所有边的xor的值,问题是如果边给的不是按照树的结构顺着根往叶子的方向给的话,看了解析,发现考点和我想象的完全不一样,我认为的path是从根结点到树中的某个结点,而题目中要的路径是从任意结点开始,到任意结点结束
忍不住看了一下解析,解析的思路是求出根节点到其它每个结点的xor值,每个结点到根结点所有边的xor值记p(x),那么任意两个结点x和y,x到y所有边上的xor值就为p(x) xor p(y),我的代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>

using namespace std;

const int N = 100004;
struct A{
    int u,v,w;
}edge[N];//这个存放边的数组

vector<int> g[N];//g[x] 表示顶点为x 的所有邻接边的边号
int visited[N],trie[N][2],weight[32];

int main()
{
    int n;
    map<int,int> m;
    weight[31] = 1;
    for(int i=30;i>=1;i--)
        weight[i] = weight[i+1] * 2;
    while(scanf("%d",&n) == 1){
        int root,x,tot = 1,mx = 0;
        for(int i=1;i<n;i++){
            scanf("%d%d%d",&edge[i].u,&edge[i].v,&edge[i].w);
            g[edge[i].u].push_back(i);
            g[edge[i].v].push_back(i);
        }
        root = edge[1].u;//随便选一个点作为根结点
        m[root] = 0;
        queue<int> q;
        q.push(root);
        while(!q.empty()){//用bfs遍历整个树 然后求得每个顶点到根结点的路径上的xor总和
            for(int t = q.size();t>0;t--){
                x = q.front();
                q.pop();
                visited[x] = 1;
                for(vector<int>::iterator it = g[x].begin();it != g[x].end();it++){
                    int nbr = *it,y,u,v;
                    u = edge[nbr].u, v = edge[nbr].v;
                    if(x == u) y = v;
                    else y = u;
                    if(!visited[y]){
                       q.push(y);
                       m[y] = m[x] ^ edge[nbr].w;
                    }
                }
            }
        }

        for(map<int,int>::iterator mit=m.begin();mit!=m.end();mit++){
            int ans = 0, val = mit->second,a = 1,p = 1;
            for(int k=1;k<=31;k++){//同上一题,边构造trie树边利用trie数求ans
                if(val >= weight[k]){//第k位为1的情况
                    if(mit != m.begin() && trie[a][0] != 0){ans += weight[k];a = trie[a][0];}
                    else if(mit != m.begin()) a = trie[a][1];
                    if(trie[p][1] == 0) trie[p][1] = ++tot;
                    p = trie[p][1];
                    val -= weight[k];
                }
                else{//第k位为0的情况
                    if(mit != m.begin() && trie[a][1] != 0){ans += weight[k];a = trie[a][1];}
                    else if(mit != m.begin()) a = trie[a][0];
                    if(trie[p][0] == 0) trie[p][0] = ++tot;
                    p = trie[p][0];
                }
            }
            mx = max(ans,mx);
        }
        printf("%d\n",mx);
        m.clear();//为了下一次测试 将一些关键结构初始化
        for(int i=0;i<n;i++) {g[i].clear();visited[i] = 0;}
        for(int k=0;k<=tot;k++) trie[k][0] = trie[k][1] = 0;
    }
    return 0;
}

示例数据通过了,不知道为什么是wrong answer,查了半天的错 还是没发现,有点郁闷,去康康光盘里的代码,发现这个可以不用map的 用map慢了,因为所有的点的编号是小于n的 所以最后建树的时候可以之间遍历数组就行
还是不知道这个代码为什么错,错误示例又找不到,很烦
我这个代码里面有两个部分 第一个部分是bfs构造数组每个点的映射,第二个部分是根据映射来求最大值
我把第二部分换成光盘里的代码 wrong answer,感觉是我的bfs错了
我再比较比较 发现我的trie数组开小了,应该是N个数 每个数有31个结点,应该选31N 或者 32N
改了以后 发现变成TLE 我想可能是我的vector导致的超时
改成数组操作 就AC了 AC 代码:32320K 860MS

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>

using namespace std;

const int N = 100004;
int n, d[N], trie[N*33][2], tot;
int Head[N], Edge[N*2], Leng[N*2], Next[N*2], num,weight[32];
bool v[N];

void dfs(int x) {
	for (int i = Head[x]; i; i = Next[i]) {//注意Head 和 Next里面存的都是边的序号,0号边是不存在的,所以0可以用作哨兵
		int y = Edge[i], z = Leng[i];
		if (v[y]) continue;
		v[y] = 1;
		d[y] = d[x] ^ z;
		dfs(y);
	}
}

void add(int x, int y, int z) {
	Edge[++tot] = y;
	Leng[tot] = z;
	Next[tot] = Head[x];
	Head[x] = tot;
}

int main()
{
    int n;
    weight[31] = 1;
    for(int i=30;i>=1;i--)
        weight[i] = weight[i+1] * 2;
    while(scanf("%d",&n) == 1){
        int mx = 0;
        memset(d, 0, sizeof(d));
        memset(trie, 0, sizeof(trie));
        memset(v, 0, sizeof(v));
        memset(Head, 0, sizeof(Head));
        num = 0;
        v[0] = 1;//因为有n-1个边说明 0,1,...,n-1个点 都是有的 即点0是存在的
        tot = 1;
        for (int i = 1; i < n; i++) {
            int u, v, w;
            scanf("%d %d %d", &u, &v, &w);
            add(u, v, w);//加入u指向v的边 边的权值为w
            add(v, u, w);
        }
        dfs(0);
        for(int c=0;c < n;c++){
            int ans = 0,a = 1,p = 1;
            for(int k=1;k<=31;k++){
                if(d[c] >= weight[k]){
                    if(c > 0 && trie[a][0] != 0){ans += weight[k];a = trie[a][0];}
                    else if(c > 0) a = trie[a][1];
                    if(trie[p][1] == 0) trie[p][1] = ++tot;
                    p = trie[p][1];
                    d[c] -= weight[k];
                }
                else{
                    if(c > 0 && trie[a][1] != 0){ans += weight[k];a = trie[a][1];}
                    else if(c > 0) a = trie[a][0];
                    if(trie[p][0] == 0) trie[p][0] = ++tot;
                    p = trie[p][0];
                }
            }
            mx = max(ans,mx);
        }
        printf("%d\n",mx);
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值