文字列とハッシュ・セット

114253 ワード

わが校OJ:テストアドレス図書セットOJ:テストアドレス

A. Oulipo


Descriptionは2つの文字列s 1,s 2(大文字のみ)を与え、s 1がs 2に何回現れるかを求める.例えば、s 1=「ABA」、s 2=「ABAABA」で、答えは2です.
解題構想:KMPアルゴリズムのテンプレートは、拡張KMPで解決することもできます.コードの例:
#include
#include 
const int N = 1e6+10; 
int nex[N];		//nex[i] = T[i,n-1] T[0,n-1]  
char S[N],T[N]; //S ,T  
int extend[N];	 

void getNex(char* str){
	/* nex */
	int len = strlen(str) ,i = 0, j, p0 ;
	nex[0] = len;
	while(i+1 < len && str[i] == str[i+1]) i++;
	nex[1] = i; p0 = 1;
	for(int i = 2;i < len;i++){
		if(nex[i-p0]+i < nex[p0]+p0) nex[i] = nex[i-p0];
		else {
			j = nex[p0]+p0-i;
			if(j < 0) j = 0;
			while(i+j < len && str[j] == str[j+i]) j++;
			nex[i] = j; p0 = i;
		}
	}
} 
void exKMP(char* str1,char *str2){
	/* str2 str1 , extend */ 
	int i = 0,j,p0,l1 = strlen(S), l2 = strlen(T);
	getNex(str2);
	while(i < l1 && i < l2 && str1[i] == str2[i]) i++;
	extend[0] = i;p0 = 0;
	for(int i = 1;i < l1;i++){
		if(nex[i-p0]+i < extend[p0]+p0) extend[i] = nex[i-p0];
		else{
			j = extend[p0]+p0-i;
			if(j < 0) j = 0;
			while(i+j < l1 && j < l2 && str1[j+i] == str2[j]) j++;
			extend[i] = j; p0 = i;
		}
	}
}
int solve(){
	/* */	
	int l1 = strlen(S), l2 = strlen(T), ans = 0;
	exKMP(S,T); int len = strlen(T);
	for(int i = 0;i < l1;i++)
		if(extend[i] == len) ans++;
	return ans;
}
int t;
int main(){
	scanf("%d",&t);
	while(t--){
		scanf("%s%s",T,S);
		printf("%d
"
,solve()); } return 0; }

B.図書管理


Description図書管理は非常に煩雑な仕事であり、図書館には毎日多くの新刊書が参加している.図書をより便利に管理するために(本を借りたいお客様が必要な本があるかどうかを迅速に検索するのを助けるために)、図書検索システムを設計する必要があります.このシステムでは、2つの操作をサポートする必要があります.
  • add(s)は、sという本が新たに加わったことを示す.
  • find(s)は、sという本があるかどうかを問い合わせる.

  • 解題構想:この問題はhashで解決するつもりだったが、私はサボって直接mapを使った.のコードの例:
    #include
    using namespace std;
    map<string ,int> mp;
    string str;
    int n;
    void add(string s){
    	string tmp = "";
    	for(int i = 4;s[i];i++) tmp += s[i];
    	mp[tmp] = 1;
    }
    void Find(string s){
    	string tmp = "";
    	for(int i = 5;s[i];i++) tmp += s[i];
    	if(mp.count(tmp)) cout << "yes" << endl;
    	else cout << "no" << endl;
    }
    int main(){
    	scanf("%d",&n);
    	getchar();
    	for(int i = 1;i <= n;i++){
    		getline(cin,str);
    		if(str[0] == 'a') add(str);
    		else Find(str);
    	}
    	return 0;
    }
    

    C. Power Strings


    Descriptionでは、長さ≦10^6の文字列がいくつか与えられ、各文字列が最大何個の同じサブ文字列で繰り返し接続されているかを尋ねます.例えば、abababは最大3つのabが接続されている.解題構想:本題はループ節問題であり,文字列sがある長さxのサブストリングループから構成されると,必ずs[1,n−x]=s[x,n]があると結論した.一方,nex配列の定義によれば,nex[n]はs[x,n]の長さn−xである.したがって、この問題に解がある場合、n%(n-nex[n])は0であり、答えはn/(n-nex[n])である.コード例:
    #include
    #include
    #include
    using namespace std;
    const int N = 1e6+10;
    char str[N];
    int nex[N];	 
    void getNex(char* str){
    	memset(nex,0,sizeof nex);
    	for(int i = 2,j = 0;str[i];i++){
    		while(j > 0 && str[i] != str[j+1]) j = nex[j];
    		if(str[i] == str[j+1]) j++;
    		nex[i] = j;
    	}
    }
    void solve(){
    	/* */
    	getNex(str);
    	int n = strlen(str)-1;
    	if(n%(n-nex[n])) puts("1");
    	else printf("%d
    "
    ,n/(n-nex[n])); } int main(){ while(scanf("%s",str+1) && str[1] != '.'){ str[0] = '*'; solve(); } return 0; }

    D. Seek the Name, Seek the Fame


    Descriptionで指定されたいくつかの文字列(これらの文字列の総長≦4)×10^5)は,各文字列において,接頭辞であり接尾辞であるすべてのサブ列長を求める.例えば、ababcabababcababは、接頭辞であり、接尾辞でもある:ab、ababab、abababcabab、ababcababababcabab.
    解題の構想:依然としてKMPアルゴリズムの中のnex配列を利用して解く.まず、この列自体が最も長い「接頭辞であり接尾辞である」サブ列であり、次の条件を満たすサブ列はs[1,nex[n],次はs[1,nex[nex[n]],....
    道理は簡単だが、思い出すと少し回りくどい.長さxのサブ列son「接頭辞であり接尾辞である」が存在する場合、s[1,x]=s[n-x,n]=sonがある場合、その長さはnex[n]を超えないのではないでしょうか.したがって、上記の方法は、すべての可能な長さ(すべてのサブストリングではない!)を大きくから小さくまで巡回することができる.
    コードの例:
    #include
    #include
    #include
    using namespace std;
    const int N = 4e5+10;
    char str[N];
    int nex[N];
    void getNex(char *s){
    	memset(nex,0,sizeof nex);
    	int len = strlen(s);
    	for(int i = 2,j = 0;i < len;i++){
    		while(j > 0 && s[i] != s[j+1]) j = nex[j];
    		if(s[i] == s[j+1]) j++;
    		nex[i] = j;
    	}
    }
    int ans[N];
    void solve(){
    	getNex(str); int n = strlen(str)-1;
    	int cnt = 0; ans[++cnt] = n;
    	while(nex[n]){
    		ans[++cnt] = nex[n];
    		n = nex[n];
    	}
    	for(int i = cnt;i > 1;i--) printf("%d ",ans[i]);
    	printf("%d
    "
    ,ans[1]); } int main(){ //freopen("123.txt","r",stdin); while(~scanf("%s",str+1)){ str[0] = '#'; solve(); } return 0; }

    E. friends


    Descriptionには3人の親友が一緒にゲームをするのが好きで、A君は次の文字列Sを書いて、B君はそれをコピーしてTを得て、C君はTの任意の位置(首尾を含む)に1つの文字を挿入してUを得る.今あなたはUを手に入れました.Sを見つけてください.
    解題の構想:本題はnexあるいはhashで解くならば、細部は少し多くて、もちろん細心の注意を払うのは書くことができるはずです.私の解題の構想はとても簡単で、O(N)複雑度、主に以下の基本的な推論に基づいています:
  • nが偶数の場合、解はありません.
  • 前のn/2+1文字が1文字削除され、後のn/2文字に等しくなる場合、マッチングは成功する.
  • 後n/2+1文字が1文字削除され、前n/2文字と同等になる場合、マッチングは成功する.
  • 2 2と3の両方が一致に成功し、2つの得られた元の列が異なる場合、解は一意ではないことを示す.

  • コードの例:
    #include
    #include
    const int N = 2e6+10;
    char str[N],tmp[N],s1[N],s2[N];
    int n,nex[N],tot;
    char ans1[N],ans2[N];
    bool check(char *s1,char *s2){
    	tot = 0; bool flag = false;
    	int i = 1,j = 1;
    	while(i <= n/2 && j <= n/2+1){
    		if(s1[i] != s2[j]){
    			if(flag) return false;
    			flag = true; j++;
    			if(s1[i] != s2[j]) return false;
    		}
    		tmp[tot++] = s1[i];
    		i++ , j++;
    	}
    	tmp[tot] = '\0';
    	return true;
    }
    void solve(){
    	if(n%2 == 0){
    		puts("NOT POSSIBLE"); return;
    	}
    	for(int i = 1;i <= n/2;i++) s1[i] = str[i];
    	for(int i = 1;i <= n/2+1;i++) s2[i] = str[i+n/2];
    	bool flag1,flag2;
    	flag1 = check(s1,s2);
    	for(int i = 0;i <= n/2;i++) ans1[i] = tmp[i];
    	for(int i = 1;i <= n/2+1;i++) s1[i] = str[i];
    	for(int i = 1;i <= n/2;i++) s2[i] = str[i+n/2+1];
    	flag2 = check(s2,s1);
    	for(int i = 0;i <= n/2;i++) ans2[i] = tmp[i];
    	if(flag1 && flag2){
    		bool f = true;
    		for(int i = 1;i <= n/2;i++) 
    			if(ans1[i] != ans2[i]) f = false;
    		if(!f){
    			puts("NOT UNIQUE"); return;
    		} 
    	}
    	if(flag1 || flag2){
    		if(flag1) puts(ans1);
    		else puts(ans2);
    		return;
    	}
    	puts("NOT POSSIBLE");
    }
    int main(){
    	scanf("%d%s",&n,str+1);
    	str[0] = '#'; solve();
    	return 0;
    }
    

    F. A Horrible Poem


    Descriptionは、小文字の英字からなる文字列Sを与え、Sのあるサブストリングの最短ループ節に答えるようにq個の質問を与える.文字列Bが文字列Aの循環節である場合、AはBによって何度も繰り返されて得られる.
    問題を解く構想:これは最短サイクルの問題で、また本題カードは常に非常に厳しい.本論文では,迅速に解くために,線形ふるい,スクロールハッシュ最適化などの戦略も用いた.
    まず、文字列ループの知識に基づいています.文字列sがあるサブストリングループから得られる場合、|s|=nのループセグメントの長さは、最短ループセグメントを含むlenの約数であるに違いない.したがって、1つの戦略は、nのすべての約数に対して、最短サイクルセグメントの長さであるか否かを1つずつ判断することであり、判断原理は以下の通りである.
  • 文字列sの最短サイクルセグメント長がxである場合、必然的にs[1,x]=s[n−x,n]がある.

  • ハッシュテクニックをスクロールすることによってO(1)時間で判断を完了することができる.では、現在主に費やされている時間は、nの約数を探すことであり、一般的なやり方O(n)O(sqrt n)O(n)であり、このやり方の総複雑度はO(q n)O(qsqrt n)O(qn)である.
    しかし、このカードは常に深刻で、タイムアウトします.そこで素因数分解定理を用いてO(l o g 2 n)O(log_2^n)O(log 2 n)時間内に約数分解を完了し,総時間複雑度はO(q l o g 2 n)O(q log_2^n)O(qlog 2 n)である.
    コードの例:
    #include
    #include
    #include
    using namespace std;
    const int N = 5e5+10;
    const int Q = 2e6+10;
    char str[N];
    int n,q;
    typedef unsigned long long ull;
    ull hsh[N],bse[N] , b = 31; // ,  
    
    bool check(int l,int r,int x){	
    	/* x s[l,r] */ 
    	ull h1 = hsh[r-x] - hsh[l-1]*bse[r-x+1-l];
    	ull h2 = hsh[r] - hsh[l-1+x]*bse[r-x+1-l];
    	return h1 == h2;
    }
    int v[N],primes[N];
    void getPri(){	//  
    	int cnt = 0;
    	for(int i = 2;i <= n;i++){
    		if(!v[i]){
    			primes[cnt++] = i;
    			v[i] = i;
    		}	
    		for(int j = 0;j < cnt;j++){
    			if(primes[j] > v[i] || primes[j]*i > N)
    				break;
    			v[i*primes[j]] = primes[j];
    		}
    	}
    }
    void ask(int l,int r){
    	/* s[l,r] */ 
    	int len = r-l+1, ans = len, d = len;
    	while(d != 1){
    		int tmp = v[d];
    		while(d%tmp == 0 && check(l,r,ans/tmp)) d /= tmp,ans /= tmp;
    		while(d%tmp == 0) d /= tmp;
    	}
    	printf("%d
    "
    ,ans); } void solve(){ /* hash ,v */ getPri(); bse[0] = 1; for(int i = 1;i <= n;i++){ hsh[i] = hsh[i-1]*b + str[i]-'a'; bse[i] = bse[i-1]*b; } } inline int read() { int x=0,f=1; char c=getchar(); while(c<'0'||c>'9') { if(c=='-') f=-1; c=getchar(); } while(c>='0'&&c<='9') { x=x*10+c-'0'; c=getchar(); } return x*f; } int main(){ n = read(); scanf("%s",str+1); str[0] = '#'; q = read(); solve(); for(int i = 1,l,r;i <= q;i++){ l = read(); r = read(); ask(l,r); } return 0; }

    G. Beads


    Description Zxlは一度ネックレスを作ることにしました.彼女はとても安い価格で鮮やかなサンゴのビーズを買いました.彼女は今も機械を持っています.このビーズをたくさんの塊(子串)に切ることができます.1枚にk(k>0)のビーズがあります.もしこのビーズの長さがkの倍数でなければ、最後の1枚がkより小さいものは引っ張らないでください.(ncはもったいない)、ビーズの長さが正の整数であることを保証します.Zxlは多様なネックレスが好きで、彼女のためにどのように数字kを選んでできるだけ多くの異なる子列を得るべきかに好奇心を持っていて、子列はすべて反転することができて、言い換えれば、子列(1,2,3)は(3,2,1)と同じです.Zxlに最適なkを決定し、最も異なるサブストリングを得るプログラムを書く.例えば、この一連のビーズは、(1,1,1,2,2,3,3,1,2,3,3,1,2,2,1,3,2,1,2,3,3,1,2,3,3,1,3,3,3,1,2,2,1,3,3,2,1,2,k=1のとき,3つの異なる子列:(1),(2),(3)k=2のとき,6つの異なる子列:(1,1),(1,2,2),(2,2,2),(2,2,2),(3,3),(3,1,1),(2,3,3)k=3のとき,5つの異なる子列:(1,1,1,1),(2,2,2,2,2),(3,2,2,2,3),(3,2,2,2,3),(3,1,2,2)k=4のとき,5つの異なる子列:(1,1,1,1,1,1,2),(2,2,2,3,3,(3,2,2,2,2,2,3),(3,2(3,1,2,2),(1,3,3,2)
    解題構想:最初は時間複雑度の計算が間違っていたが,n+n/2+n/3+n/4+...+1は調和級数であり,時間複雑度は高くないため,2層サイクルにローリングハッシュO(1)を加えた判断は速やかに通過できる.
    コードの例:
    #include
    using namespace std;
    const int N = 2e5+10;
    typedef unsigned long long ull;
    int col[N] , n;
    ull hsh[N],hsh2[N],bse[N] , B = 1e9+7;
    map<ull,int> vis;
    vector<int> ans;
    int tc , mx , k;
    void solve(){
    	bse[0] = 1; int cnt = 0;
    	for(int i = 1;i <= n;i++) 
    		hsh[i] = hsh[i-1]*B+col[i], bse[i] = bse[i-1]*B;
    	for(int i = n;i >= 1;i--) 
    		hsh2[i] = hsh2[i+1]*B+col[i];
    	for(int i = 1;i <= n;i++){
    		vis.clear(); cnt = 0;
    		for(int j = i;j <= n;j += i){
    			ull h1 = hsh[j] - hsh[j-i]*bse[i];
    			ull h2 = hsh2[j-i+1] - hsh2[j+1]*bse[i];
    			if(vis.count(h1*h2) == 0){
    				vis[h1*h2] = 1; cnt++;
    			}
    			//printf("%llu %llu
    ",h1,h2);
    } //printf("%d %d
    ",cnt,mx);
    if(cnt == mx) ans.push_back(i); else if(cnt > mx) { ans.clear(); ans.push_back(i); mx = cnt; } } printf("%d %d
    "
    ,mx,ans.size()); for(int i = 0;i < ans.size();i++) printf("%d ",ans[i]); } int main(){ scanf("%d",&n); for(int i = 1;i <= n;i++) scanf("%d",col+i); solve(); return 0; }

    H. Antisymmetry


    Descriptionは、01文字列について、この文字列0と1を逆にしてから、その文字列全体を元の文字列と逆にすると、「反対称」文字列と呼ばれます.例えば00001111と010101は反対で、1001はそうではありません.次に、Nの長さの01文字列を与え、どのくらいのサブ列が反対称であるかを求める.
    解題構想:01列の問題は本当に変化が多いですね.この問題を解決するには、まずいくつかの推論を出す必要があります.
  • 「反対称」の列長は偶数に違いない.
  • サブストリングが「反対称」である場合、それは必ず中軸線に関し、左右01は対応する(例えば0101).
  • もし1つの列が「反対称」であれば、それと共中軸線のサブ列も「反対称」である.

  • そして,エコー列の性質と少し似ていることが分かったが,2点だけ異なるのは,エコー列の長さが奇抜であり,エコー列は逆ではなく中軸対称であることである.
    では、Manacherアルゴリズムを利用することができますが、判決などを「逆」に変更すればいいだけです.もちろん、ワイルドカード「#」で埋めます.すべての「反対称」の列の長さは偶数でなければならないので、「#」で計算して、長さが偶数であることを保証します.
    コードの例:
    #include
    using namespace std;
    const int N = 5e5+10;
    char str[N],s2[2*N];
    int n,len[2*N];
    bool check(char a,char b){
    	if(a == '#' && b == '#') return true;
    	if(a == '1' && b == '0') return true;
    	if(a == '0' && b == '1') return true;
    	return false;
    }
    void solve(){
    	s2[0] = '$',s2[1] = '#';
    	for(int i = 1;i <= n;i++){
    		s2[i<<1] = str[i-1];
    		s2[(i<<1)+1] = '#';
    	}
    	s2[n*2+2] = '\0';
    	int mx = 0, mid;
    	for(int i = 1;i < 2*n+2;i += 2){
    		if(i < mx) len[i] = min(len[2*mid-i],mx-i);
    		else len[i] = 1;
    		while( check(s2[i-len[i]], s2[i+len[i]]) ) len[i]++;
    		if(len[i]+i > mx){
    			mx = len[i] + i;
    			mid = i;
    		}
    	}
    	long long ans = 0;
    	for(int i = 1;i < 2*n+2;i++) ans += len[i]>>1;
    	printf("%lld
    "
    ,ans); } int main(){ //freopen("123.txt","r",stdin); scanf("%d",&n); scanf("%s",str); solve(); return 0; }

    I.チケット(tickets)


    Description既知数列a n,a 0=1 a_n, a_0 = 1 an​,a0​=1, a i + 1 = ( a i ∗ A + a i   m o d   B )   m o d   C a_{i+1}=(a_i*A+a_i:mod:B):mod:C ai+1=(ai
    解題構想:mapは遅すぎるでしょう.私は読み込み出力をテストして、乗算と型取りをテストしました.mapを疑っていません.多くの時間を無駄にしました.手書きhash関数は,隣接テーブルを用いて衝突を解消し,これでかなり速くなった.
    コードの例:
    #include
    #include
    typedef long long ll;
    const int N = 2181271;
    ll a,b,c;
    double A;
    struct Node{
    	ll dat;
    	Node* nex;
    	Node(ll dat):dat(dat){nex = NULL;}
    	Node(){ nex = NULL; }
    }head[N];
    bool check(ll x){
    	int h = x%N;
    	//printf("%d %lld
    ",h,x);
    Node* p = head[h].nex; while(p != NULL){ //printf("%lld
    ",p->dat);
    if(p->dat == x) return true; p = p->nex; } return false; } void Add(ll x){ int h = x%N; // printf("%d %lld
    ",h,x);
    Node* p = head[h].nex; Node *q = &head[h]; while(p != NULL) q = p,p = p->nex; q->nex = new Node(x); //printf("%lld
    ",p->dat);
    } void solve(){ ll res = 1; Add(res); for(int i = 1;i < 2e6;i++){ res = (res*a+res%b)%c; if(check(res)){ printf("%d
    "
    ,i); return; } Add(res); } puts("-1"); return; } int main(){ //freopen("123.txt","r",stdin); scanf("%lld%lld%lld",&a,&b,&c); solve(); return 0; }

    J.雪花収集(snowflakes)


    Descriptionの異なる雪は往々にして異なる形をしている.北の学生は雪を集めて、南方の学生たちにプレゼントしたいと思っています.全部でn個の時刻があり,各時刻に雪が降る形状を与え,異なる整数で異なる形状を表す.収集の過程で、学生たちは繰り返しの雪があることを望んでいない.任意のa時刻から、b時刻に停止することができます.aからbまでの間の雪も集められる.彼らは収集した雪が一番多いことを望んでいる.
    解題の構想:これはhashで少し書きにくくて、ハッシュ表を空にするのは難しいです.私は離散化+ダブルポインタを使っています.左境界を維持するだけでいいです.現在の雪が2回目に現れると、前回の出現位置がv[p]であることがわかるので、まず現在の区間長で答えを更新し、左境界をv[p]+1に更新します.正確性証明書は書かないのは明らかだ.
    コードの例:
    #include
    using namespace std;
    typedef long long ll;
    const int N = 1e6+10;
    int a[N], b[N], n, v[N];
    int main(){
    	//freopen("123.txt","r",stdin);
    	scanf("%d",&n);
    	for(int i = 1;i <= n;i++) scanf("%d",a+i),b[i] = a[i];
    	sort(a+1,a+1+n);
    	int tot = unique(a+1,a+1+n)-a-1;
    	int ans = 0,l = 1;
    	for(int i = 1;i <= n;i++){
    		int p = lower_bound(a+1,a+tot+1,b[i])-a;
    		if(v[p] >= l){
    			ans = max(ans,i-l);
    			l = v[p]+1; 	
    		}
    		v[p] = i;
    		if(i == n) ans = max(ans,i-l+1);
    	}
    	printf("%d
    "
    ,ans); return 0; }