来自学长的字符串

我们一起备考 NOI Plus

Fedya the Potter Strikes Back

一上午过去了……

关于当前时刻的所有子串,它的前缀提前算出来的答案可以直接加,所以只考虑在原有的答案上修改最后一个i添加的贡献,发现他是一堆border,只是多加上了它自己。本来打算每加一次就让它跳所有的nxt,每次求区间最小值(线段树单点修改区间查询),似乎会T。

但其实产生贡献的border也可以从前面继承,之前的border在续上一个字母s[i+1]后依然有效的条件是s[i+1]==s[lenx+1]。除了长度为1和全部的特殊情况,其它的贡献一定全都来自继承,对每一种合法的情况的前后分别删掉最后一个字符,就得到了上一个的某个border。

于是不只有最终答案可以从上一个累加,新的单步答案也可以由上一个单步答案修改得到。

考虑单步答案的修改。特殊情况直接加入,把上一个的border分成两组:对当前位置合法的和不合法的。

不合法的

  • 不合法的贡献需要删掉,如果可以知道不合法border的左端点和右端点就可以查到这个区间最小值。为了在i快速找到每个以i-1结尾的不合法border的长度,我们要在i进行预处理为i+1做准备。
  • 把每一种字母当成颜色得到26个组,i的所有border可以通过不停地跳nxt找到,找到构成border的前缀部分,按照它的下一项分组。比较不合法的时候找到不合法的下一项,就可以从长到短的找出所有。如果把每一组当成一条链,就只需要连接上一个,结果就是按颜色分开的26棵fail树。
  • 假设现在循环到i,已知i的一个border的长度是nxt[i],循环到i+1时这个border不被删除要求s[i+1]==s[nxt[i]+1],因为s[i+1]还未知,把这个border分到第s[nxt[i]+1]组里备用。

合法的

  • 它会继续贡献,但是它贡献的大小可能改变,新加入的数可能修改区间最小值。记录每一个贡献的大小和它贡献的次数,二分找到比a[i]大的全都修改成a[i],并且把它的出现次数累记到a[i]的出现次数上。过程大概比较暴力。
  • 第一次发现map的自动排序派上用场。

于是我们愉快地得到了单步的答案,但是还有细节。

这个总数会爆long long,可以选择开个高精度,但是也可以开两位long long表示答案。%018lld表示不够18位在左侧补0,取模也很神奇

int operator % (BIG x, int y)
{
    return (x.ans1 % y + (x.ans2 % y) * (di % y) % y) % y;
}

还有位运算,&((1<<30)-1)表示只有末30位有效,并且只要ans对应是1运算结果就是1,所以它就相当于对(1<<30)取模。

关于fail树 我本来一直以为是一个多么高级的东西……

Thanks a lot.

code是鹤的

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 6e5 + 3;
const ll di = 1e18;
const ll MASK = (1 << 30);//把&变成%
const ll inf = 0x3f3f3f3f3f3f3f3f;

int n, nxt[maxn], fail[maxn][26];
ll a[maxn], sum, sta[maxn];
char s[maxn], c[3];
map<ll, int> mp;

struct BIG 
{
    ll ans1, ans2;
}ans;

BIG operator + (BIG x, ll y)
{
    return (BIG){(x.ans1 + y) % di, x.ans2 + (x.ans1 + y)/di};
}

int operator % (BIG x, int y)
{
    return (x.ans1 % y + (x.ans2 % y) * (di % y) % y) % y;
}

void write(BIG x)
{
    if(x.ans2) printf("%lld%018lld\n", x.ans2, x.ans1);
    else printf("%lld\n", x.ans1);
}

inline ll read()
{
    ll x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

struct seg 
{
    struct node 
    {
        ll min;
    }t[maxn<<2];
    void pushup(int x)
    {
        t[x].min = min(t[x<<1].min, t[x<<1|1].min);
    }
    void update(int x, int l, int r, int pos, ll val)
    {
        if(l == r)
        {
            t[x].min = val; return;
        }
        int mid = (l + r) >> 1;
        if(pos <= mid) update(x<<1, l, mid, pos, val);
        else update(x<<1|1, mid+1, r, pos, val);
        pushup(x);
    }
    ll query(int x, int l, int r, int L, int R)
    {
        if(L <= l && r <= R) return t[x].min;
        int mid = (l + r) >> 1; ll ans = inf;
        if(L <= mid) ans = min(ans, query(x<<1, l, mid, L, R));
        if(R > mid) ans = min(ans, query(x<<1|1, mid+1, r, L, R));
        return ans;
    }
}t;

void Insert(ll x)
{
    int num = 0; sta[0] = 0;
    for(map<ll, int>::iterator it=mp.lower_bound(x); it!=mp.end(); it++)
    {
        sta[++sta[0]] = (*it).first; num += (*it).second; sum -= (*it).first * (*it).second;
    }
    while(sta[0]) mp.erase(sta[sta[0]--]);
    mp[x] += num; sum += x * num;
}

int main()
{
    n = read();
    scanf("%s", c); s[1] = c[0];
    a[1] = read(); t.update(1, 1, n, 1, a[1]); ans = (BIG){a[1], 0};
    write(ans);
    for(int i=2,j=0; i<=n; i++)
    {
        scanf("%s", c); s[i] = c[0];
        a[i] = read(); 
        s[i] = (s[i]-97+(ans%26)%26)%26+97; a[i] = a[i]^(ans%MASK);

        while(j && s[i] != s[j+1]) aj = nxt[j];
        if(s[i] == s[j+1]) j++; nxt[i] = j;
        for(int k=0; k<26; k++)
        {
            if('a'+k == s[nxt[i]+1]) fail[i][k] = nxt[i];
            else fail[i][k] = fail[nxt[i]][k];
        }
        t.update(1, 1, n, i, a[i]); ans = ans + t.query(1, 1, n, 1, i);

        if(s[i] == s[1])
        {
            mp[a[i]]++; sum += a[i];
        }
        for(int k=0; k<26; k++)
        {
            if('a'+k != s[i])
            {
                for(int now=fail[i-1][k]; now; now=fail[now][k])
                {
                    ll val = t.query(1, 1, n, i-1-now+1, i-1);
                    mp[val]--; sum -= val;
                }
            }
        }
        Insert(a[i]);
        ans = ans + sum; write(ans);
    }

    return 0;
}

 

OKR-Periodicity

一个下午就要过去了……

我发现我才知道周期period的概念:如果对于一个长度为n的字符串s,所有的i∈[1, n-p]都有s[i]==s[i+p],p是s的一个周期。所以存在一个长度为m的period与存在一个长度为n-m的border等效。

把s的border集合(由nxt构成)升序排序,排完后得到a1,a2……an,可以先构造a1再构造a2……,构造出最小的period的方法就是,如果它的长度是1就填一个0,否则填(长度-1)个0和一个1。

为什么可以从小到大依次构造,因为周期是从1开始的,之后都要对应相等,那么短的一定被长的包含。

  • 2*a[i-1]>=a[i]只需要将ans[i-1]的后a[i]-a[i-1]位复制到ans[i-1]的后面。
  • 2*a[i-1]<a[i] :首先ans[i-1]一定会在a[i]的头尾出现两次,如果中间直接全填0的话可能会多出来其它的border,先填上0检查ans[i]是否存在一个长度为a[i]-a[i-1](就是tat中的ta的长度)约数的period。
  • 实时找到ans的nxt数组可以另起一个名字fail,找到它的nxt那么它的长度减nxt就是它的period的最小值,只需要检查最小值因为更大的period一定是它的倍数!
  • 感觉这个用来说明倍数关系更合适!

这里用到了Weak Periodicity Lemma:

对于一个字符串s,若p和q都是它的周期且p+q<=|s|,gcd(p, q)也是s的周期。

证明就是假设p<q向前跳p个再向后跳q个,s[i]==s[i-p]==s[i-p+q]于是不断得到他们的差值。因为p=q<=|s|所以第一步要么可以往前跳p要么可以往后跳q,先往后跳的话i+q-p>=1一定成立因为i是正的,q-p是正的!先前再后超范围了就算了。但其实感觉前面、第一次跳出去了都没关系,能跳回里面就算数。

  • 回到刚才的检查:可行的话直接填0,否则把这堆0的最后一个改成1就可以了。

为什么要求最后一个0改1就可以:考虑整除这个条件是怎么来的,啊我好想画个图!画个图就会发现会不会出现新的border取决于中间这一段能不能被共用,也就是它是不是既可以当结尾又可以当开头,满足这个就要求它自己是一个从最开头开始计算有一个最小的循环节可以被t+a整除。

感谢神仙们的详细证明

鹤一下---我只有%

code
 #include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 2e5 + 3;

char s[maxn];
int T, n, m, p, nxt[maxn], a[maxn], fail[maxn], ans[maxn];

inline int read()
{
    int x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

void ins(int c)
{
    ans[++p] = c;
    if(p > 1)
    {
        int j = fail[p-1];
        while(j && ans[j+1] != ans[p]) j = fail[j];
        if(ans[j+1] == ans[p]) j++;
        fail[p] = j;
    }
}

int main()
{
    T = read();
    while(T--)
    {
        scanf("%s", s+1); n = strlen(s+1);
        for(int i=2,j=0; i<=n; i++)
        {
            while(j && s[j+1] != s[i]) j = nxt[j];
            if(s[j+1] == s[i]) j++;
            nxt[i] = j;
        }
        a[m = 0] = p = 0;
        for(int i=n; i; i=nxt[i]) a[++m] = i;
        for(int i=1; i<=m/2; i++) swap(a[i], a[m+1-i]);
        if(a[1] == 1) ins(0);
        else 
        {
            for(int i=1; i<a[1]; i++) ins(0);
            ins(1);
        }
        for(int i=2; i<=m; i++)
        {
            if(a[i-1]*2 >= a[i]) for(int j=a[i-1]+1,k=a[i]-a[i-1]; j<=a[i]; j++) ins(ans[j-k]);
            else 
            {
                for(int j=a[i]-2*a[i-1]; j; j--) ins(0);
                if(!(p%(p-fail[p]))) p--, ins(1);
                for(int j=1; j<=a[i-1]; j++) ins(ans[j]);
            }
        }
        for(int i=1; i<=n; i++) printf("%d", ans[i]);
        printf("\n");
    }

    return 0;
}

 

论战捆竹竿

这个题花费的time更长了……

鹤题解之前:

数据范围显示不出来是什么鬼?算出每个border的长度,n-borderi就是可以添加的值的所有情况,重叠的部分都相等并且由于记录长度所以加入的顺序并不重要,同种长度可以一起的话个数还无限,那就是一个完全背包问题?由于起点是n,初始化f[0]=0,答案应该是f[w-n],盲猜TLE+MLE。

打算先去做跳楼机学习一下同余最短路。

插入:同余最短路

同余最短路通常用来解决给定的n个数能拼成多少个在范围内其它数的问题。

对于n个数中的任意数x,对于p∈[0, x],把p当成一个组,如果p+k*x可以被拼出来,那么p+k*(任意正数*x)也可以被拼出。设tmp=p+kmin*x,p组更大的数也可以被拼出,答案是(mx-tmp)/x+1。

关于后面的+1,把除法看成求一个大区间划分成几个小区间,求出来的这些个数标记上区间右端点,发现最左边也就是tmp自己是空着的,加的1就是tmp。大区间的左端点是f[i]右端点是mx,区间长度应该是mx-f[i]+1,在除法之前-1为mx-f[i],除完后+1,这个就是向上取整。

跳楼机
 #include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 1e5 + 3;

ll h, x, y, z, f[maxn], ans;
bool vis[maxn];

inline ll read()
{
    ll x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

struct node 
{
    int next, to, w;
}a[maxn<<1];
int head[maxn], len;

void add(int x, int y, int w)
{
    a[++len].to = y; a[len].next = head[x]; a[len].w = w;
    head[x] = len;
}

queue<int> q;
void spfa()
{
    memset(f, 0x3f, sizeof(f));
    q.push(1);
    vis[1] = 1;
    f[1] = 1;
    while(!q.empty())
    {
        int x = q.front(); q.pop();
        vis[x] = 0;
        for(int i=head[x]; i; i=a[i].next)
        {
            int y = a[i].to;
            if(f[y] > f[x] + a[i].w)
            {
                f[y] = f[x] + a[i].w;
                if(!vis[y])
                {
                    q.push(y);
                    vis[y] = 1;
                }
            }
        }
    }
}

int main()
{
    h = read(); x = read(); y = read(); z = read();
    if(x == 1 || y == 1 || z == 1) {printf("%lld\n", h); exit(0);}
    for(int i=0; i<x; i++)
    {
        add(i, (i+y)%x, y);
        add(i, (i+z)%x, z);
    }
    spfa();
    for(int i=0; i<x; i++)
    {
        if(f[i] <= h) ans += (h-f[i])/x+1;
    }
    printf("%lld\n", ans);

    return 0;
}

再回到这个题,发现学习前置知识同余最短路是很有必要的,and以下很多内容都是对题解的解释,逻辑可能也不够清晰,只有一些细节,建议结合题解阅读。

于是首先可以用同余最短路把上文提到的完全背包优化一下,但是复杂度依然不对。

所有长度大于等于字符串一半的border构成一个等差序列,还有结合上一题用到的gcd(p, q)也是周期,可以得到这些周期构成等差序列,它不仅是有倍数关系并且是真的连续的等差序列!!我认为重合的部分是border,(背包物品)应该接的是period。

upd(对于下文):补充一下为什么会形成gcd(x, d)个环:两个不同的环上的点不能相互到达,从起点往后的gcd(x, d)个位置永远不会被经过,以它们为起点都会得到新的环,关于剩下的位置有没有可能成为起点,想象把环上的点分成gcd组,每一个点都有一个归属那就是它所在的环。

把这些等差序列分开考虑,关于y向(y+d)%mod x连边构成gcd(x, d)个环(x是首项,d是公差,y是%x的某个余数),画一些图发现是对的,并且这些环的起点是从0开始连续的一段(对下文的处理很有用),然而并不会证明。其实y向(y+d)%mod连边已经是对建边的一种优化了,这个等差序列是x, x+d, x+2*d……,根据同余最短路,应该是y向(y+x+d)%x,(y+x+2*d)%x,(y+x+2*d)%x连边,前边的x就很多余,把它去掉就成了y向y+d连长度为d的边,向y+2*d连长度为2*d的边,到了y+d,y+d要向2*d连长度为d的边,发现和前面的重复,于是每个y只需要向后连一条边!然后就得到了连完所有边之后的图。

对一个环跑最短路是一个不明智的选择,从dij出发,以dis最小的点为起点,它不能被更新于是环就断成了链,每个点只能更新他的下一个,更不了就算了用下一个的初值(更优的)继续更新,如果没有长度限制的话直接二倍长(这就是换模数时的操作)。关于一个等差序列的长度限制,我们把连边简化了,但是简化之前一个值能传递的范围就是等差序列的终点,简化之后把很多边当成一条边跑最短路,依然需要注意这个限制,就需要单调队列。

关于dis[i]怎么跨模数转移当时想了半天,发现再好好理解一下同余最短路中的概念就解决了(dis[i]是真实的高度,它是否可以被表示与模数无关!)。题解上最有启示意义的句子是dis[i](f[i])的含义是dis[i]+k*las(las>=0)可以被访问,于是解释了dis[i]和后来公差为las的都可以去更新

那么为什么最优答案已经被记录了却要用原来已经被排除过的答案继续更新呢?因为只记录最小值会漏掉很多东西:考虑一个特殊的等差数列(或者说不特殊的例子也有不少,找就很多),首项是x,公差也是x,那么始终只有f[0]有值,并且只存了最小的那个。在换模数的时候,它只能一个去向,但是y和2*y对同一个数取模救过不一定相同,也就是在上一个模数下不优的答案在新的模数下可能是优的,于是解释了dis[i]后来公差为las的都一定要去更新。

还有这个now=n的初始化,我不清楚它不能删掉的原因,可能是因为n太小没有模数变化所以变成0了??但是先KMP把它改成period[1]也可以过,感觉这个period[1]更好理解一些。

if(diff < 0) return;

我认为这个是不可能出现的,但是把它删掉WA 0,思考了一下觉得:

但是这句话不能删!?啊我首先找到了一个:如果i就是序列的末尾!!
序列的末尾是什么?是一个n,它是不是一个周期还有争议。。因为0不是border,但是它是一个可以往上接的合法竹子!
然而它可以提供一个正确的公差!它只对上一项有用,它不可以作为一个等差序列的起点吗?只有自己?
所以它不需要去更新,只需要被更新!!也就是为什么要先换模数再返回!!
其它的不合法diff应该是真的没了。。
code
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 5e5 + 2;
const ll inf = 0x3f3f3f3f3f3f3f3f;//8个3f

int T, n, ct, nxt[maxn], period[maxn], pos[maxn], seq[maxn], Q[maxn<<1], now;
ll f[maxn], res[maxn], ans, w, sta[maxn];
char s[maxn];

inline ll read()
{
    ll x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

void get_fail()
{
    int sz = strlen(s+1);
    for(int i=2,j=0; i<=sz; i++)
    {
        while(j && s[j+1] != s[i]) j = nxt[j];
        if(s[j+1] == s[i]) j++;
        nxt[i] = j;
    }
    int now = nxt[sz];
    while(now)
    {
        period[++ct] = sz - now;
        now = nxt[now];
    }
    period[++ct] = sz;
}

void change_mod(int mod)
{
    int cnt = __gcd(mod, now);
    for(int i=0; i<now; i++) res[i] = f[i];
    for(int i=0; i<mod; i++) f[i] = inf;
    for(int i=0,tmp; i<now; i++)
    {
        tmp = res[i] % mod; f[tmp] = min(f[tmp], res[i]);
    }
    for(int i=0; i<cnt; i++)
    {
        int top = 0;
        Q[++top] = i;
        int tmp = (i + now) % mod;
        while(tmp != Q[1])
        {
            Q[++top] = tmp, tmp = (tmp + now) % mod;
        }
        for(int j=top+1; j<=2*top; j++)
        {
            Q[j] = Q[j-top];
        }
        top <<= 1;
        for(int j=2; j<=top; j++)
        {
            f[Q[j]] = min(f[Q[j]], f[Q[j-1]] + now);
        }
    }
    now = mod;
}

void work(int first, int diff, int siz)
{
    int cnt = __gcd(diff, first);
    change_mod(first);
    if(diff < 0) return;
    for(int i=0; i<cnt; i++)
    {
        int top = 0;
        Q[++top] = i;
        int tmp = (i + diff) % first;
        while(tmp != Q[1])
        {
            Q[++top] = tmp, tmp = (tmp + diff) % first;
        }
        int mini_pos = 1;
        for(int j=1; j<=top; j++)
        {
            if(f[Q[j]] < f[Q[mini_pos]]) mini_pos = j;
        }
        int tmp_cnt = 0;
        for(int j=mini_pos; j<=top; j++)
        {
            seq[++tmp_cnt] = Q[j];
        }
        for(int j=1; j<mini_pos; j++)
        {
            seq[++tmp_cnt] = Q[j];
        }
        int head = 1, tail = 1;
        pos[1] = 1, sta[1] = f[seq[1]] - diff;
        for(int j=2; j<=top; j++)
        {
            while(head <= tail && pos[head] + siz < j) head++;
            if(head <= tail) f[seq[j]] = min(f[seq[j]], sta[head]+j*(ll)diff+first);
            while(head <= tail && sta[tail] >= f[seq[j]]-j*(ll)diff) tail--;
            sta[++tail] = f[seq[j]]-j*(ll)diff, pos[tail] = j;
        }
    }
}

int main()
{
    T = read();
    while(T--)
    {
        ans = ct = 0;
        n = read(), w = read(); w -= n;
        memset(f, 0x3f, sizeof(ll[n]));
        f[0] = 0;
        now = n;//或者把它注释掉
        scanf("%s", s+1);
        get_fail();
        //在这里加上now = period[1];
        for(int i=1,j=1; i<=ct; i=j)
        {
            while(period[j+1]-period[j] == period[i+1]-period[i]) j++;
            work(period[i], period[i+1]-period[i], j-i-1);
        }
        for(int i=0; i<now; i++)
        {
            if(f[i] <= w) ans += (w - f[i]) / now + 1;
        }
        printf("%lld\n", ans);
    }

    return 0;
}

 

posted @ 2022-11-10 19:13  Catherine_leah  阅读(68)  评论(3编辑  收藏  举报
/* */