日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

CSP-S集训刷题记录

發(fā)布時(shí)間:2023/12/10 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 CSP-S集训刷题记录 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

$ CSP.S $ 集訓(xùn)刷題記錄:

$ By~wcwcwch $





一、字符串專題:



1. 【模板】$ manacher $ 算法

模型:

求出字符串 $ S $ 中所有回文串的位置及長(zhǎng)度。

$ solution $ :

個(gè)人理解:解決這類問(wèn)題,回文串的對(duì)稱性質(zhì)最重要。

于復(fù)雜度最關(guān)鍵的一句話: $ f[i]=min~(~r-i~,~f[~mid\times2-i~]~)~ $ (實(shí)現(xiàn)不同,邊界可能不一樣)

這個(gè) $ min $ 函數(shù)左邊 $ r-i $ 是當(dāng)前位置到它所屬于的回文串邊界的距離,右邊 $ mid\times 2 -1 $ 是它在所屬于的回文串的另一邊的對(duì)應(yīng)位置。如果取左邊,那么我用當(dāng)前位置擴(kuò)展(會(huì)超出當(dāng)前回文串邊界)于是必然能夠使 $ r $ 向右擴(kuò)展;如果取右邊,我們發(fā)現(xiàn)由于回文串的對(duì)稱性,這個(gè)位置必然不能繼續(xù)向外擴(kuò)展,因?yàn)樗铋_(kāi)始向外擴(kuò)展必然還是在當(dāng)前回文串邊界內(nèi)(否則 $ min $ 函數(shù)肯定取的左邊),而如果它能在當(dāng)前回文串內(nèi)擴(kuò)展,它的對(duì)稱位置 $ mid\times 2 -1 $ 也能,而 $ f[~mid\times2-i~] $ 已是擴(kuò)展最大值,與之矛盾。

總結(jié)一下,如果 $ min $ 函數(shù)取左邊,那么 $ r $ 一定能向右擴(kuò)展;如果取右邊,那么復(fù)雜度為 $ 1 $ (常數(shù))。于是復(fù)雜度為 $ r $ 的值域和 $ 1 $ 的次數(shù),兩者都不超過(guò) $ n $ ,于是復(fù)雜度為 $ O(2\times n) $ ,就算再加上與處理時(shí)加“#”號(hào),也是常數(shù)而已。



2. 【模板】擴(kuò)展 $ KMP $

模型:

有一個(gè)字符串 $ a $ ,要求輸出【 $ a $ 本身】和【 $ a $ 的每一個(gè)后綴】的最長(zhǎng)公共前綴 。

$ solution $ :

$ KMP $ 和 $ Manacher $ 思想的結(jié)合產(chǎn)物。不過(guò)個(gè)人更傾向于 $ Manacher $ 。因?yàn)樗挠?jì)算方法和 $ Manacher $ 神似,都是在某一個(gè)位置先根據(jù)之前的結(jié)果得到一個(gè)初始的公共前綴長(zhǎng)度,就是“回文串”變成了最長(zhǎng)公共前綴,回文串的中心 $ mid $ 變成了最長(zhǎng)公共前綴的左端點(diǎn) $ l $ ,最后 $ min $ 函數(shù)長(zhǎng)的不一樣了。

我們對(duì)于 $ a $ 處理出它【每一位開(kāi)始的后綴】和【 $ a $ 本身】的最長(zhǎng)公共前綴 $ f[i] $ 。注意我們要從第二位開(kāi)始 ,因?yàn)?$ f[1]=|a| $ 很特殊。我們用 $ l $ 記錄已經(jīng)遍歷過(guò)的 $ i $ 中 $ i+f[i] $ 最大的 $ i $ ,同時(shí)用 $ r $ 記錄 $ i+f[i] $ ,這兩者初始均為 $ 0 $ 。然后: $ f[i]=min(~r-i~,~f[i-l+1]~) $ ,這個(gè)計(jì)算初始值的式子和 $ Manacher $ 很像, $ i $ 處于一個(gè)公共前綴里,那么 $ i $ 后面的字母一定和 $ f[i-l+1] $ 后面的字母在 $ r-i $ 范圍內(nèi)保持一致,于是直接拿來(lái)作為初始值即可。復(fù)雜度證明和上面 $ Manacher $ 一樣!

$ code $ :
int n,m; int f[2000005]; string s,a;int main(){cin>>a; n=a.size();cin>>s; m=s.size();s=' '+s+'#'+a; n+=m+2; //加到一起rg l=0,r=0; f[1]=m; //第一個(gè)不管for(rg i=2;i<=n;++i){if(i<=r) f[i]=min(r-i,f[i-l+1]); //確定初始值while(s[f[i]+1]==s[i+f[i]]) ++f[i]; //向后擴(kuò)展if(i+f[i]-1>r) l=i,r=i+f[i]-1; //更新l和r的值}for(rg i=1;i<=m;++i) printf("%d%c",f[i],i==m?'\n':' ');for(rg i=m+2;i<n;++i) printf("%d%c",f[i],i==n-1?'\n':' ');return 0; }

3. $ LOJ~3095 $ : $ Snoi~2019 $ 字符串

題意概括:

給一個(gè)長(zhǎng)度為 $ n $ 的字符串 $ S $ ,記 $ S′_i $ 表示 $ S $ 刪去第 $ i $ 個(gè)字符后得到的字符串,輸出:\(( S′_1 , 1)...(S′_n , n)\) 的字典序排序結(jié)果。 $ n\leq 10^6 $

$ solution $ :

我們仔細(xì)觀察題目,手算分析樣例,可以發(fā)現(xiàn)一個(gè)性質(zhì):

  • 設(shè)從 $ i $ 號(hào)位置開(kāi)始,往后第一個(gè)與 $ i $ 號(hào)位不同的位置為 $ j $ ,則 $ [i,j-1] $ 的字符串相等。
  • 若 $ a[i]<a[j] $ ,則可以發(fā)現(xiàn)后面的字符串因?yàn)槎喟粋€(gè) $ a[i] $ 而小于 $ [i,j-1] $ 。
  • 若 $ a[i]>a[j] $ ,則可以發(fā)現(xiàn)后面的字符串因?yàn)槎喟粋€(gè) $ a[i] $ 而大于 $ [i,j-1] $ 。
  • 于是我們開(kāi)一個(gè)雙端加入的數(shù)組,記錄最后的答案。期望復(fù)雜度: $ O(n) $

    $ code $ :
    n=qr(); cin>>a; a=' '+a; a[n+1]=')'; //防出界rg l=0,r=n+1; //記錄雙端指針for(rg i=1;i<=n;++i){ rg j=i;while(a[i]==a[i+1])++i; //找到連續(xù)相同串的右端點(diǎn)if(a[i+1]<a[i]) for(rg k=j;k<=i;++k) s[++l]=k; //這一段都必然比后面的串小else for(rg k=i;k>=j;--k) s[--r]=k; //這一段都必然比后面的串大}for(rg i=1;i<=n;++i) printf("%d ",s[i]);

    4. 洛谷 $ P5446 $ : $ THUPC~2018 $ 綠綠和串串

    題意概括:

    對(duì)于字符串 $ S $ ,定義運(yùn)算 $ f(S) $ 為將 $ S $ 的前 $ |S|-1 $ 個(gè)字符倒序接在 $ S $ 后面形成的長(zhǎng)為 $ 2|S|-1 $ 的新字符串。現(xiàn)給出字符串 $ T $ ,詢問(wèn)有哪些串長(zhǎng)度不超過(guò) $ |T| $ 的串 $ S $ 經(jīng)過(guò)若干次 $ f $ 運(yùn)算后得到的串包含 $ T $ 作為前綴。$ 1 \le |T| \le 5 \times 10^6. $

    $ solution $ :

    很糾結(jié)的一道題,調(diào)了好一會(huì)才發(fā)現(xiàn)思維不夠嚴(yán)謹(jǐn),判斷出錯(cuò)了。

    首先我們不難發(fā)現(xiàn)這是一道與回文串有關(guān)的題目,我們可以發(fā)現(xiàn)如果 $ S $ 串中存在一個(gè)位置 $ i $ 使得它的最長(zhǎng)回文串能到達(dá) $ S $ 串的末尾,那么對(duì) $ [1,i] $ 這一段字符進(jìn)行 $ f(1,i) $ 的操作一定可以得到一個(gè)合法新字符串使 $ S $ 為其前綴。然后我們還可以發(fā)現(xiàn)如果將這些位置標(biāo)記,如果 $ S $ 串中還有一些位置 $ i $ 使得從 $ i $ 擴(kuò)展的回文串可以到達(dá) $ S $ 串的第一個(gè)字符且這個(gè)回文串的末尾字符是被標(biāo)記了的,那么這個(gè)位置也是合法的!因?yàn)橹灰M(jìn)行多次 $ f $ 操作即可。

    $ code $ :
    t=qr();while(t--){cin>>s; n=s.size(); s=' '+s; //讀入rg mid=0,r=0; s[0]='('; s[n+1]=')'; //設(shè)邊界for(rg i=1;i<=n;++i){ f[i]=0;if(i<r) f[i]=min(r-i,f[(mid<<1)-i]); //manacher尋找初值while(s[i-f[i]-1]==s[i+f[i]+1]) ++f[i]; //擴(kuò)展if(i+f[i]>r) mid=i,r=i+f[i]; //更新} k[n]=1;for(rg i=n;i>=1;--i) //如果轉(zhuǎn)一次就合法,或者能夠連轉(zhuǎn)多次if(i+f[i]==n||(i-f[i]==1&&k[i+f[i]])) k[i]=1;for(rg i=1;i<=n;++i)if(k[i])printf("%d ",i),k[i]=0; //輸出+清零puts("");}

    5. 洛谷 $ P4503 $ :$ CTSC~2014~$ 企鵝 $~QQ $

    題意概括:

    給出一個(gè)帶通配符的字符串 $ S $ ,通配符分兩種,一種可以匹配恰好一個(gè)字符,另一種可以匹配任意個(gè)(包括 $ 0 $ 個(gè))字符。再給出 $ n $ 個(gè)串 $ T_i $ ,詢問(wèn) $ T_i $ 能否與 $ S $ 匹配。 $ 1 \le n \le 100, 1 \le |S|,|T_i| \le 10^5, 0 \le $ 通配符個(gè)數(shù) $ \le 10. $

    $ solution $ :

    總算搞了一道 $ Hash $ 題了,應(yīng)該算是第一道仔細(xì)調(diào)了細(xì)節(jié)的代碼。對(duì)于 $ Hash $ 我們有單哈希和多哈希,根據(jù)元素信息的維數(shù)來(lái)看。但是個(gè)人還是喜歡單哈希里的 $ long~long $ ,效果和雙 $ int $ 哈希差不多。 $ long~long $ 好寫,跑的飛快,但是模數(shù)太大容易溢出;雙 $ int $ 哈希(一般用 $ STL:pair $ ),跑的不快,但是正確性高很多。

    這道題我們可以采取暴力措施,因?yàn)橹挥幸晃豢梢圆煌?#xff0c;所以我們干脆枚舉這一位是哪一位,然后將每個(gè)字符串除開(kāi)這一位的前綴和后綴相互比較,如果相同那么就是合法的一對(duì)。前綴和后綴的比較可以預(yù)處理 $ Hash $ 得到。

    期望復(fù)雜度: $ O(m\times n\times logn) $

    $ code $ :
    const ll mod=20190816170251; //紀(jì)念CCF關(guān)門的日子是個(gè)質(zhì)數(shù) //小常識(shí):1e18+3和1e18+9都是質(zhì)數(shù)很好記,998244353和998244853和993244853也是。ll ans; int n,m,k; ll q[30005]; ll f[30003][202]; //前綴hash ll s[30003][202]; //后綴hash char a[30005];int main(){n=qr(); m=qr(); k=qr();for(rg i=1;i<=n;++i){scanf("%s",a+1); //scanf讀入快一點(diǎn) //107這個(gè)數(shù)不要太大,防溢出for(rg j=1;j<=m;++j) f[i][j]=(f[i][j-1]*107+a[j])%mod; //前綴hashfor(rg j=m;j>=1;--j) s[i][j]=(s[i][j+1]*107+a[j])%mod; //后綴hash} ll x=1; //模數(shù)不能太大,否則這里會(huì)炸掉for(rg i=1;i<=m;++i){ //每個(gè)位置分開(kāi)做會(huì)快些,正確性也高一些for(rg j=1;j<=n;++j)q[j]=f[j][i-1]*x+s[j][i+1]; //前后合并sort(q+1,q+n+1);for(rg j=1,o=1;j<=n;j=++o){while(q[j]==q[o+1]) ++o; //找到連續(xù)一段相同的ans+=((ll)(o-j+1)*(o-j))>>1; //注意答案貢獻(xiàn)為C[x][2],從一段中取一對(duì)} x=x*107%mod;}printf("%lld\n",ans);return 0; }

    6. 洛谷 $ P3167 $ : $ CQOI~2014 $ 通配符匹配

    題意概括:

    給出一個(gè)帶通配符的字符串 $ S $ ,通配符分兩種,一種可以匹配恰好一個(gè)字符,另一種可以匹配任意個(gè)(包括 $ 0 $ 個(gè))字符。再給出 $ n $ 個(gè)串 $ T_i $ ,詢問(wèn) $ T_i $ 能否與 $ S $ 匹配。 $ 1 \le n \le 100, 1 \le |S|,|T_i| \le 10^5, 0 \le $ 通配符個(gè)數(shù) $ \le 10. $

    $ solution $ :

    想法頗多,但實(shí)現(xiàn)艱巨的一道題,寫了一晚上加一上午。

    講課時(shí)和 $ Itst $ 討論覺(jué)得是 $ AC $ 自動(dòng)機(jī),結(jié)果大鴿鴿一講是 $ Hash $ ,題解第一篇居然也是 $ Hash $ ?!但是 $ AC $ 自動(dòng)機(jī)本就和 $ Hash $ 完成的東西一樣,都要求兩段子串是否相等, $ AC $ 自動(dòng)機(jī)預(yù)處理, $ Hash $ 在線 $ O(1) $ 求,所以同出一源。

    首先這道題的突破口是通配符無(wú)疑!因?yàn)閿?shù)量太少,我們可以根據(jù)通配符劃分模式串,得到不多于 $ 10 $ 個(gè)模式串。然后我們可以考慮在給的 $ n $ 個(gè)串中找到這些模式串的位置,然后想辦法合并這些位置以判斷是否合法。想法是好的,實(shí)現(xiàn)起來(lái)卻十分殘酷,首先 $ AC $ 自動(dòng)機(jī)找串復(fù)雜度 $ |S|^2 $ ,直接撲街;但是我們很快發(fā)現(xiàn)模式串不多,而且只需要知道其是否在某個(gè)位置出現(xiàn),我們考慮狀壓,每次不暴力訪問(wèn),直接位運(yùn)算(期望復(fù)雜度 $ O(|S|) $ )。

    然后我們發(fā)現(xiàn)實(shí)現(xiàn)這個(gè)匹配是非常難的。但是我們也可以發(fā)現(xiàn)匹配是線性的按順序的匹配,和動(dòng)態(tài)規(guī)劃很像,于是我們考慮動(dòng)態(tài)規(guī)劃,對(duì)每一個(gè)位置記錄他可以匹配完哪一個(gè)的模式串。然后我們對(duì)于每一個(gè)狀態(tài)(某一個(gè)位置是某一個(gè)模式串的末尾)可以根據(jù)這個(gè)模式串之前是哪一個(gè)通配符來(lái)轉(zhuǎn)移,如果是 $ ? $ 那么直接找到模式串開(kāi)頭前一個(gè)位置,看它是否匹配了上一個(gè)模式串;如果是 $ * $ 就找之前所有狀態(tài),是否有匹配了上一個(gè)模式串的位置(用前綴或(位運(yùn)算)和)。利用狀壓我們可以直接位運(yùn)算解決所有轉(zhuǎn)移。

    $ code $ :
    #include<iostream> #include<cstdio> #include<iomanip> #include<algorithm> #include<cstring> #include<cstdlib> #include<ctime> #include<cmath> #include<vector> #include<queue> #include<map> #include<set>#define ll long long #define db double #define rg register intusing namespace std;int n,m,t; int s[10005]; int b[10005]; int mx[100005]; int f[100005]; int k[100005]; char a[100005];struct AC{int tt;int end[100005];int fail[100005];int son[100005][26];inline void add(int l,int r,int v){rg now=0,*to;for(rg i=l;i<=r;++i){to=&son[now][a[i]-'a'];if(*to)now=*to;else now=*to=++tt;}end[now]|=(1<<v); //將這個(gè)模式串編號(hào)狀壓到自動(dòng)機(jī)里面}inline void failed(){queue<int> q;for(rg i=0;i<26;++i)if(son[0][i]){end[son[0][i]]|=end[0]; //fail樹的應(yīng)用,這個(gè)位置的字符串包含哪些模式串q.push(son[0][i]);}rg *to,next;while(!q.empty()){rg i=q.front(); q.pop(); next=fail[i];for(rg j=0;j<26;++j){to=&son[i][j];if(*to){ q.push(*to);fail[*to]=son[next][j];end[*to]|=end[son[next][j]]; //fail樹的應(yīng)用,這個(gè)位置的字符串包含哪些模式串}else *to=son[next][j];}}}inline void find(int l){ //找到文本串里每個(gè)位置對(duì)應(yīng)哪些模式串末尾rg now=0; f[0]=end[now];for(rg i=1;i<=l;++i){now=son[now][a[i]-'a'];f[i]=end[now]; //這個(gè)位置是那些模式串的末尾}} }T;inline int qr(){register char ch; register bool sign=0; rg res=0;while(!isdigit(ch=getchar()))if(ch=='-')sign=1;while(isdigit(ch))res=res*10+(ch^48),ch=getchar();if(sign)return -res; else return res; }int main(){scanf("%s",a+1); m=strlen(a+1); a[++m]='s'; //最后加個(gè)字符是為了防通配符在末尾for(rg i=1,j=1,x=0;i<=m;i=++j){while(j<=m&&a[j]>='a'&&a[j]<='z')++j; //以通配符為界,劃分為若干模式串T.add(i,j-1,++t); s[(1<<t)]=x; b[(1<<t)]=j-i; //記錄上一個(gè)通配符是哪一種,以及串長(zhǎng)if(j<=m&&a[j]=='?') x=1; else x=2; //辨別這個(gè)通配符是哪一種}T.failed(); rg n=qr();for(rg i=1;i<=n;++i){ //每個(gè)待匹配串都視為文本串,跑AC自動(dòng)機(jī)scanf("%s",a+1); m=strlen(a+1);a[++m]='s'; T.find(m); k[0]=mx[0]=1; //預(yù)處理+初始化for(rg j=1;j<=m;++j) k[j]=mx[j]=0; //初始化for(rg j=0;j<=m;++j){for(rg x=f[j];x;x-=x&-x){ //這層循環(huán)的復(fù)雜度為通配符個(gè)數(shù)rg y=x&-x;if(s[y]==0) k[j]|=y&(k[j-b[y]]<<1); //如果之前沒(méi)有通配符if(s[y]==1) k[j]|=y&(k[j-b[y]-1]<<1); //上一個(gè)通配符是?就直接看前面對(duì)應(yīng)位置是否可以匹配if(s[y]==2) k[j]|=y&(mx[j-b[y]]<<1); //上一個(gè)通配符是*就直接看全局,因?yàn)橹虚g字符可消mx[j]|=k[j]; //這個(gè)對(duì)于*操作很重要,記錄這個(gè)位置之前的最高匹配度} mx[j+1]=mx[j]; //這個(gè)對(duì)于*操作很重要,記錄這個(gè)位置之前的最高匹配度}if(k[m]&(1<<t))puts("YES"); //看最后一位的匹配度是否達(dá)到最高else puts("NO");}return 0; }

    7. 洛谷 $ P3193 $ : $ HNOI~2008~GT $ 考試

    題意概括:

    給一個(gè)數(shù)字串 $ S $ ,求有多少長(zhǎng)度為 $ n $ 的數(shù)字串 $ T $ 不包含 $ S $ 作為子串。 $ 1 \le |S| \le 100, 1 \le n \le 10^9. $

    $ solution $ :

    并沒(méi)有想象中(上一道題)的這么難,唯獨(dú)就是要寫嚴(yán)格 $ O(1) $ 的 $ kmp $ 算法。感覺(jué)比普通的還好寫一點(diǎn)。

    首先我們求的串中必須不包含 $ S $ ,這個(gè)我們其實(shí)不難想到動(dòng)態(tài)規(guī)劃。設(shè) $ F[i][j] $ 表示已經(jīng)構(gòu)造到第 $ i $ 個(gè)位置,在末尾最多有 $ j $ 個(gè)位置和 $ S $ 的前 $ j $ 個(gè)位置一致。之所以這樣設(shè)狀態(tài)是因?yàn)槲覀儤?gòu)造合法串時(shí)只需要知道串后面的字符和 $ S $ 的匹配程度,這樣可以知道我們放下一個(gè)字符會(huì)不會(huì)使得一段后綴變?yōu)?$ S $ (任何一個(gè)子串都可以表示為一個(gè)前綴的后綴)。然后我們就需要轉(zhuǎn)移方程,這個(gè)其實(shí)就是一個(gè) $ kmp $ 的過(guò)程。但是為了快速轉(zhuǎn)移我們需要知道在當(dāng)前串后面加入某個(gè)字符會(huì)使匹配轉(zhuǎn)移到哪一個(gè)位置!這個(gè)需要用嚴(yán)格 $ O(1) $ 的 $ kmp $ 算法。再然后我們就可以將這個(gè)轉(zhuǎn)移過(guò)程用矩陣來(lái)完成,通過(guò)改變矩陣系數(shù)使得一次轉(zhuǎn)移等同一次乘法。然后快速冪。

    $ code $ :
    int n,m,k; int nx[105]; int f[105][10]; char a[105];struct su{ //矩陣int s[101][101];inline void operator *=(const su &x){ //矩陣乘法su res;for(rg i=0;i<m;++i){for(rg j=0;j<m;++j){rg y=0;for(rg k=0;k<m;++k)y+=s[i][k]*x.s[k][j];res.s[i][j]=y%k; //有時(shí)候?qū)⑷∧7磐饷鏁?huì)快些}} *this=res;}inline su operator ^(int y){ //矩陣快速冪su res,x=*this;for(rg i=0;i<m;++i) res.s[i][i]=1;while(y){if(y&1)res*=x;x*=x; y>>=1;}return res;} }ans,xx;int main(){n=qr(); m=qr(); k=qr();scanf("%s",a+1);ans.s[0][0]=1;for(rg i=1;i<=m;++i){nx[i]=f[i-1][a[i]-'0']; //似乎比不嚴(yán)格的好寫?f[i-1][a[i]-'0']=i; //f[i][j]表示從這一位在后面加字符j會(huì)匹配到哪個(gè)位置for(rg j=0;j<=9;++j) //復(fù)雜度比一般kmp高一點(diǎn)f[i][j]=f[nx[i]][j]; //就是嚴(yán)格O(1)的kmp}for(rg i=0;i<m;++i)for(rg j=0;j<=9;++j)++xx.s[i][f[i][j]]; //加入矩陣系數(shù)ans*=xx^n; int tot=0; //矩陣快速冪for(rg i=0;i<m;++i)tot+=ans.s[0][i]; //統(tǒng)計(jì)所有不含所給串的串的個(gè)數(shù)printf("%d\n",tot%k);return 0; }

    8. 洛谷 $ P2414 $ $ NOI~2011 $ 阿貍的打字機(jī)

    題意概括:

    給你一棵 $ n $ 個(gè)節(jié)點(diǎn)的 $ Trie $ 樹, $ q $ 次詢問(wèn) $ Trie $ 樹上 $ x $ 節(jié)點(diǎn)代表的字符串在 $ y $ 節(jié)點(diǎn)代表的字符串中出現(xiàn)了多少次。

    $ 1 \le n, q \le 10^6. $

    $ solution $ :

    很難的一道題,可以說(shuō)是很多算法的結(jié)合: $ AC $ 自動(dòng)機(jī), $ fail $ 樹, $ dfs $ 序,樹狀數(shù)組

    首先是讀入,一般讀入肯定超時(shí),因?yàn)榉胚M(jìn) $ AC $ 自動(dòng)機(jī)的字符串很多很長(zhǎng),但是他們很多前綴相同。于是根據(jù)題意模擬,我們?cè)?$ trie $ 數(shù)上跑操作序列,只要記錄每一個(gè)節(jié)點(diǎn)的父親,就可以支持加入字符后的撤銷操作,然后打印操作直接在當(dāng)前 $ trie $ 的節(jié)點(diǎn)處打標(biāo)機(jī)即可(詳細(xì)見(jiàn)代碼)

    然后是關(guān)于 $ fail $ 樹,我們對(duì)于每一個(gè) $ fail $ 指針件一條反向邊,因?yàn)橐粋€(gè)節(jié)點(diǎn)只有一個(gè) $ fail $ 指針,得到的圖一定是棵有根樹。然后我們思考這棵樹的意義:(假設(shè)每個(gè)節(jié)點(diǎn)對(duì)應(yīng)一個(gè)從根到這個(gè)節(jié)點(diǎn)的字符串)我們?cè)谄胀ㄇ闆r下從某個(gè)節(jié)點(diǎn)(對(duì)應(yīng)字符串 $ S $ )沿 $ fail $ 指針跑,遍歷的字符串都是 $ S $ 的子串且是后綴!于是反過(guò)來(lái),我們?cè)?$ fail $ 樹上從某個(gè)節(jié)點(diǎn)(對(duì)應(yīng) $ S $ )遍歷其子樹,遍歷到的字符串都一定包含 $ S $ 作為子串,且 $ S $ 是它們后綴!

    然后我們還知道,一個(gè)子串一定可以表示成母串的前綴的后綴。于是如果我們要在 $ trie $ 上找 $ x $ 節(jié)點(diǎn)代表的字符串在 $ y $ 節(jié)點(diǎn)代表的字符串中出現(xiàn)了多少次,我們只需要將 $ y $ 字符串的每一個(gè)前綴對(duì)應(yīng)節(jié)點(diǎn)(在 $ trie $ 上為到根鏈)都標(biāo)記,然后在 $ fail $ 樹上的 $ x $ 好節(jié)點(diǎn)開(kāi)始遍歷子樹,有多少節(jié)點(diǎn)被標(biāo)記那么 $ x $ 節(jié)點(diǎn)代表的字符串在 $ y $ 節(jié)點(diǎn)代表的字符串中出現(xiàn)了多少次。

    但是我們發(fā)現(xiàn)詢問(wèn)很多,字符串長(zhǎng)度很大。但是我們可以離線:因?yàn)樽址乃星熬Y對(duì)應(yīng)節(jié)點(diǎn)在 $ trie $ 上為一條到根的鏈,我們馬上回憶到一種題型(在 $ dfs $ 一棵樹時(shí)維護(hù)節(jié)點(diǎn)到根路徑的信息:在遍歷到某一節(jié)點(diǎn)時(shí)加入節(jié)點(diǎn)貢獻(xiàn),在離開(kāi)節(jié)點(diǎn)時(shí)刪掉節(jié)點(diǎn)貢獻(xiàn))。再聯(lián)想一下我們需要知道 $ fail $ 的子樹信息和,我們就可以想到先找到樹的 $ dfs $ 序,用樹狀數(shù)組維護(hù)以 $ dfs $ 序?yàn)閷?duì)應(yīng)位置的數(shù)組,然后遍歷整棵 $ trie $ 樹,在遍歷到某一節(jié)點(diǎn)時(shí)給樹狀數(shù)組對(duì)應(yīng)位置加一,在離開(kāi)節(jié)點(diǎn)時(shí)減一。然后如果遍歷 $ trie $ 樹時(shí),當(dāng)前節(jié)點(diǎn)為詢問(wèn)中的 $ y $ 字符串(所有前綴都以標(biāo)記),就到樹狀數(shù)組里找到對(duì)應(yīng)詢問(wèn) $ x $ 字符串對(duì)應(yīng)位置的子樹和。

    $ code $ :
    #include<iostream> #include<cstdio> #include<iomanip> #include<algorithm> #include<cstring> #include<cstdlib> #include<ctime> #include<cmath> #include<vector> #include<queue> #include<map> #include<set>#define ll long long #define db double #define rg register intusing namespace std;int tim; int n,m,tot; int a[100005]; int tr[100005]; int dfn[100005]; int low[100005]; int ans[100005];struct bian{int to,next; }b[1000005]; int tou[100005],top;struct lian{int to,res,id,next; }q[100005]; int hd[100005],len;struct AC{int tt;int fa[100005];int end[100005];int fail[100005];int son[100005][26];bool vis[100005][26];inline void add(const string &ch){rg now=0,*to,l=ch.size();for(rg i=0;i<l;++i){if(ch[i]>='a'&&ch[i]<='z'){to=&son[now][ch[i]-'a'];if(*to) now=*to; else *to=++tt,fa[*to]=now,now=*to;}if(ch[i]=='B') now=fa[now]; //額外計(jì)一個(gè)父親用來(lái)撤銷if(ch[i]=='P') a[++n]=now;}}inline void failed(){queue<int> q;for(rg i=0;i<26;++i){if(son[0][i]){fail[son[0][i]]=0;q.push(son[0][i]);}}rg *to,next;while(!q.empty()){rg i=q.front(); q.pop(); next=fail[i];for(rg j=0;j<26;++j){to=&son[i][j];if(*to){ q.push(*to);fail[*to]=son[next][j];}else vis[i][j]=1,*to=son[next][j];} //因?yàn)橹笮枰闅v原始字典樹,所以需要一個(gè)vis數(shù)組記錄更改}}inline void get_eg(){for(rg i=1;i<=tt;++i){ rg x=fail[i],y=i;b[++top]=bian{y,tou[x]}; tou[x]=top;} //根據(jù)fail指針建fail樹的邊(反邊)} }ac;inline int qr(){register char ch; register bool sign=0; rg res=0;while(!isdigit(ch=getchar()))if(ch=='-')sign=1;while(isdigit(ch))res=res*10+(ch^48),ch=getchar();if(sign)return -res; else return res; }inline void add(int x,int v){for(;x<=tim;x+=x&-x) tr[x]+=v; } //需要注意樹狀數(shù)組的范圍inline int ask(int x){ rg res=0;for(;x;x-=x&-x) res+=tr[x];return res; }inline void yu(int i){dfn[i]=++tim;for(rg j=tou[i];j;j=b[j].next) yu(b[j].to);low[i]=tim; } //有向樹的dfs序,dfn和low記錄子樹區(qū)間inline void dfs(int i){add(dfn[i],1);for(rg j=hd[i];j;j=q[j].next){q[j].res=ask(low[q[j].to])-ask(dfn[q[j].to]-1);} //用鏈?zhǔn)角跋蛐怯涗浽儐?wèn)for(rg j=0;j<26;++j)if(ac.son[i][j]&&!ac.vis[i][j])dfs(ac.son[i][j]);add(dfn[i],-1); } //遍歷原始字典樹是因?yàn)槊恳粋€(gè)子串都是前綴的后綴 //字典樹遍歷所有前綴,而fail樹的dfs序與前綴的后綴有關(guān)int main(){//freopen(".in","r",stdin);//freopen(".out","w",stdout);string ch; getline(cin,ch); ac.add(ch);ac.failed(); ac.get_eg();yu(0); m=qr();for(rg i=1;i<=m;++i){rg x=a[qr()],y=a[qr()]; //將詢問(wèn)具體到哪個(gè)字符串q[i]=lian{x,0,i,hd[y]}; hd[y]=i;} dfs(0);for(rg i=1;i<=m;++i) ans[q[i].id]=q[i].res;for(rg i=1;i<=m;++i) printf("%d\n",ans[i]);return 0; }

    9. 拯救紫萱學(xué)姐(試題)

    題意概括:

    定義對(duì)于兩個(gè)字符串 $ a $ 和 $ b $ ,如果 $ a $ 既是 $ b $ 的前綴也是 $ b $ 的后綴,那么稱 $ a $ 和 $ b $ 相似(空字符串和任何字符串相似)。一個(gè)字符串可以通過(guò)編輯變換成一個(gè)比它短而且與它相似的字符串,付出的代價(jià)為這兩個(gè)字符串的長(zhǎng)度之差的平方。兩個(gè)字符串通過(guò)編輯而變?yōu)?strong>同一個(gè)字符串所花費(fèi)的最小代價(jià)被稱為最短編輯距離。現(xiàn)給定一個(gè)字符串,你需要知道這個(gè)字符串的每一對(duì)前綴的最短編輯距離中的最大值是多少。

    $ 1 \le |S| \le 10^6 $

    $ solution $ :

    其實(shí)看到 $ a $ 既是 $ b $ 的前綴也是 $ b $ 的后綴時(shí),就可以想到 $ kmp $ 思想。然后我們就會(huì)發(fā)現(xiàn)每一次編輯變換,就是沿著 $ kmp $ 的 $ next $ 數(shù)組往前跳。然后為了代價(jià)最小,我們肯定每次只跳一步。于是可以得到一個(gè)結(jié)論,對(duì)于兩個(gè)前綴他們沿 $ next $ 數(shù)組向前跳,第一個(gè)相同的位置所對(duì)應(yīng)的前綴就是最終狀態(tài)(類似于將 $ next $ 數(shù)組反向連邊,在樹上跑 $ LCA $ )。于是我們根據(jù) $ next $ 數(shù)組從后往前遍歷,每次用當(dāng)前位置跟新它 $ next[] $ 對(duì)應(yīng)位置的最大代價(jià),同時(shí)記錄答案。

    $ code $ :
    ll ans; int n; int f[2000005]; ll s[2000005]; char a[2000005];int main(){scanf("%s",a+1); n=strlen(a+1); f[1]=0;for(rg i=2,j=0;i<=n;++i){ //kmpwhile(j&&a[i]!=a[j+1]) j=f[j];if(a[i]==a[j+1]) f[i]=++j; //next數(shù)組}for(rg i=n;i>=1;--i){ll j=f[i];ll x=(ll)(i-j)*(i-j); //記錄這一步的代價(jià)ans=max(ans,s[i]+x+s[j]); //更新答案s[j]=max(s[j],s[i]+x); //更新最大代價(jià)}printf("%lld\n",ans);return 0; }

    10. 洛谷 $ P3279 $ : $ SCOI~2013 $ 密碼

    題意概括:

    給出一個(gè)長(zhǎng)度為 $ n $ 的數(shù)組 $ {p_i} $ 和一個(gè)長(zhǎng)度為 $ n $ 的數(shù)組 $ { q_i } $ ,分別表示以字符串中每個(gè)字符以及每?jī)蓚€(gè)相鄰字符的間隙為中心的最長(zhǎng)回文串長(zhǎng)度。你需要根據(jù)給出的 $ { p_i } $ 和 $ {q_i} $ ,構(gòu)造出一個(gè)滿足條件的字符串 $ S $ 。 $ n \le 10^6 $

    $ solution $ :

    將 $ manacher $ 倒著做一遍,從前往后構(gòu)造字符串。

  • 因?yàn)榛匚拇畬?duì)稱性質(zhì),前面一般直接賦值到后面一半。
  • 因?yàn)榛匚拇畬?duì)稱性質(zhì),我們記錄一個(gè) $ bool $ 數(shù)組,回文串的后面第一個(gè)字符不能等于回文串前一個(gè)字符。
  • 然后貪心放小的字符即可。

    $ code $ :
    int n; int a[1000005]; int b[1000005]; int s[1000005]; bool f[1000005][27];int main(){n=qr(); rg r=1; s[0]=26;for(rg i=1;i<=n;++i)a[i]=qr()>>1; //注意for(rg i=1;i<n;++i) b[i]=qr()>>1;for(rg i=1;i<=n;++i){if(i>=r){ ++r; //貪心放最小的for(rg j=0;j<26;++j)if(!f[i][j]){s[i]=j; break;}} rg x=i-a[i],y=i+a[i]; //記錄邊界f[y+1][s[x-1]]=1; //后面第一個(gè)字符不能等于回文串前一個(gè)字符while(r<=y) s[r]=s[i*2-r],++r;x=i-b[i]+1,y=i+b[i]; //同上f[y+1][s[x-1]]=1;while(r<=y) s[r]=s[i*2-r+1],++r;}for(rg i=1;i<=n;++i)printf("%c",s[i]+'a');puts("");return 0; }

    11. \(LOJ~6187~Odd\)

    題意概括:

    有一個(gè) $ n $ 個(gè)數(shù)的數(shù)組 $ A $ 。求有多少個(gè)子區(qū)間,滿足每個(gè)區(qū)間里出現(xiàn)過(guò)的數(shù)都只出現(xiàn)了奇數(shù)次。

    $ n \leq 2 \times 10^5, a_i \leq 10^6 $

    $ solution $ :

    很難調(diào)的一道題,細(xì)節(jié)很多,小技巧也很多。(似乎上一道我這么認(rèn)為的題也是分塊?)

    我們考慮這道題的“奇偶”二字,說(shuō)明本題很可能和狀壓、位運(yùn)算什么的有關(guān)。考場(chǎng)上以為狀壓一下、求個(gè)前綴異或和、再開(kāi)個(gè)桶就可以解決,奈何數(shù)據(jù)范圍大、“出現(xiàn)過(guò)”三字太毒瘤!但是仔細(xì)摸索一番我們不難想到 $ Hash $ 。因?yàn)槲覀円竺總€(gè)區(qū)間,所以很直接的想法就是枚舉一個(gè)端點(diǎn),尋找合法的另一個(gè)端點(diǎn)。而要快速知道一個(gè)區(qū)間內(nèi)數(shù)的奇偶性,我們只能依靠異或操作,和后綴和(通過(guò)異或操作,使得合法區(qū)間一定滿足異或和為 $ 0 $ ,這樣根據(jù)右端點(diǎn)后綴異或和就能得到左端點(diǎn)后綴異或和,然后查存后綴異或和的桶子就好)

    為了能夠方便異或操作,我們給每個(gè)值隨機(jī)一個(gè)大數(shù),這樣正確性會(huì)很高(總共后綴異或和的數(shù)量為 $ n $ 個(gè),值域?yàn)?$ 2^{63} $ ,安全)。然后我們枚舉右端點(diǎn) $ r $ 并實(shí)時(shí)維護(hù)后綴(顯然右端點(diǎn)以右的點(diǎn)的后綴異或和不需要管,因?yàn)閮纱萎惢虻?)。因?yàn)楹戏▍^(qū)間的異或和為 $ 0 $ 而區(qū)間里的數(shù)又是奇數(shù)次,所以我們可以忽略每一個(gè)數(shù)的最后一個(gè)。于是當(dāng)我們右端點(diǎn)向右移時(shí)假設(shè)下一個(gè)數(shù)為 $ v_i $ ,那么對(duì)于前面所有后綴,它肯定是里面等于 $ v $ 的書中最后一個(gè)。(假設(shè)上一個(gè) $ v $ 出現(xiàn)位置為 $ j $ )于是從 $ [1,j] $ 的所有后綴均需要異或 $ v $ ( $ [j+1,i] $ 不需要!)。然后因?yàn)橛叶它c(diǎn)后綴為 $ 0 $ ,合法區(qū)間異或和為零,我們只需要知道前面有多少位置的后綴異或和為 $ 0 $ 即可!

    然后講一下實(shí)現(xiàn),首先我們需要隨機(jī) $ rand $ 大數(shù)。然后我們需要記錄每一個(gè)位置的后綴異或和,并支持區(qū)間(某個(gè)前綴區(qū)間)異或修改,以及區(qū)間(前綴區(qū)間)查詢 $ 0 $ 的出現(xiàn)次數(shù)!因?yàn)橹涤蚝艽笏晕覀兊每紤] $ Hash $ 表查詢,對(duì)于區(qū)間修改查詢我們可以線段樹,但是線段樹被卡內(nèi)存了,僅分塊還活著!然后對(duì)于區(qū)間異或我們可用類似線段樹的 $ lazytag $ 一樣給每個(gè)塊打標(biāo)記!查詢這個(gè)塊時(shí)有多少0,就是查詢有多少 $ 0~xor~lazytag $ !(分塊操作有點(diǎn)繞,要仔細(xì)分析)

    注意 $ Hash $ 表因?yàn)榉謮K的緣故需要支持刪除和加入,我們要鏈表刪除,回收空間!不能暴力更改,空間會(huì)爆!

    $ code $ :
    ll ans; int n,m; int a[200005]; ll b[1000005]; //隨機(jī)化權(quán)值 ll s[1000005]; //后綴(隨著右端點(diǎn)向右移,會(huì)變化的后綴) int t[1000005]; //上一個(gè)出現(xiàn)位置 int f[200005]; //分塊struct Hash{int tt,top;ll vv[505]; //數(shù)值int da[505]; //數(shù)量int to[505]; //鏈表指向int stc[505]; //我刪掉的位置(回收棧)int hd[2005];inline void add(ll x,int v){rg y=x%2003; rg i=0; //先Hash一下for(rg j=hd[y];j;i=j,j=to[j]){ //鏈?zhǔn)角跋蛐谴鎯?chǔ)if(vv[j]==x){da[j]+=v; //更新權(quán)值if(!da[j]){stc[++top]=j; //計(jì)入回收棧if(!i) hd[y]=to[j]; //鏈表刪除else to[i]=to[j];} return ;}}i=top?stc[top--]:(++tt); //找到一個(gè)沒(méi)用的位置vv[i]=x; da[i]=1; to[i]=hd[y]; hd[y]=i; //存儲(chǔ)}inline int ask(ll x){for(rg j=hd[x%2003];j;j=to[j]) //鏈?zhǔn)角跋蛐窃L問(wèn)if(vv[j]==x) return da[j];return 0;} }ha[505]; ll lz[505];int main(){srand(time(NULL)); //隨機(jī)化n=qr(); m=sqrt(n-1)+1; //分塊的大小for(rg i=1;i<=n;++i) f[i]=(i-1)/m+1; //這個(gè)下標(biāo)在哪個(gè)塊for(rg i=1;i<=n;++i) a[i]=qr(),b[a[i]]=rand()+((ll)rand()<<31); //隨機(jī)權(quán)值for(rg i=1;i<=n;++i){ha[f[i]].add(0,1); //加入初始值if(t[a[i]]){ rg x=f[t[a[i]]]; ll v=b[a[i]]; //上一個(gè)出現(xiàn)的位置;該位置的隨機(jī)權(quán)值for(rg j=1;j<x;++j) lz[j]^=v; //塊上打標(biāo)記for(rg j=m*(x-1)+1;j<=t[a[i]];++j)ha[x].add(s[j],-1), s[j]^=v, ha[x].add(s[j],1); //先取出,后修改,再加入} t[a[i]]=i; //該位置變?yōu)樽詈笠粋€(gè)出現(xiàn)位置for(rg j=1;j<=f[i];++j) ans+=ha[j].ask(lz[j]); //詢問(wèn)(直接訪問(wèn)大塊就可以了!)}printf("%lld\n",ans);return 0; }



    二、搜索專題:



    $ hncpc~2019E~Numbers $

    題意概括:

    給一個(gè)數(shù)字串 $ S $ ,求將其劃分成 $ [0,99] z$ 不含前導(dǎo)零且互不相同的數(shù)的方案數(shù)。

    $ solution $ :

    湖南師范大學(xué)的 $ ACM $ 比賽題,沒(méi)有 $ source $ 。反正比較簡(jiǎn)單,直接口糊。

    爆搜,枚舉每個(gè)位置是否是小于 $ 10 $ 的位置,其他位置的數(shù)就確定了。于是邊搜索邊 $ check $ 就好了!



    題意概括:
    $ solution $ :
    $ code $ :




    三、圖論專題:



    題意概括:
    $ solution $ :
    $ code $ :




    四、數(shù)據(jù)結(jié)構(gòu)專題:



    題意概括:
    $ solution $ :
    $ code $ :


    轉(zhuǎn)載于:https://www.cnblogs.com/812-xiao-wen/p/11600101.html

    總結(jié)

    以上是生活随笔為你收集整理的CSP-S集训刷题记录的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。