FFT與一些冷門問題

GreenDuck發表於2019-03-23

FFT也能用於一些特殊的字串匹配與最小化問題。

Prob 1 : 給出模式串A與文字串B,兩個串中只有26個大寫字母與萬用字元'?'(即可以任意匹配一個字元),求A在B中的匹配數。要求以FFT為例給出上限為O(nlogn)的演算法。


Prob 2 : 給出模式串A與文字串B,字符集很小,求A在B中的匹配數,允許有k個字元不同。要求以FFT為例給出上限為O(nlogn*|S|)的演算法。


Prob 3 : 給出數列a和b,長度均為n,a可以順時針轉動但不能翻轉,最小化sigma(ai*bi)。要求以FFT為例給出上限為O(nlogn)的演算法。


不知道是什麼東西的引導

 我們先看看FFT幹了什麼,就是個卷積。

以陣列a和b為例(這裡下標從1開始),a有4位,b有8位,卷出的結果放在c陣列中。

然而並沒有什麼用處。我們再往後看幾位:

雖然FFT時會把a陣列給自動補全,但從實際意義上來講,只是整個a陣列與b陣列中四個數相乘放進c中。

不難發現,此時的下標就是一個“佔位符”。

我們順便把a陣列反一反,就有:

這樣就有很好的性質了,c陣列中從第5位開始,每往後一位就是整個a陣列與b陣列中連續的四位積的和。

同樣可以擴充到更大的陣列中,接下來的題目就要利用這個特點。


Prob 1

我們發現字串的匹配很類似於上述圖片中一位位算過去。

先不考慮萬用字元,只是普通的字串匹配。定義為A的第x位與B的第y位的匹配度。若C為0,則是匹配的。

再定義,表示B字串中以x為結尾,向前m-1位與A字串的匹配度。我們天真地考慮若P為0,則是匹配的。

但是C有正有負,因此一旦連續的幾位的可重集是相同的,P的結果就為0。

所以在C上動手腳。乾脆加個平方吧:

這樣,

但還不能優化!因此我們又看了看上面的圖,把A字串反了過來。定義

注意到(m-i-1)+(x-m+1+i)==x,有:

這樣S與B做一遍卷積就行了。S與B的值取字串的字元值就行了。

那帶上萬用字元,只要有任何字元遇上“?”,C的值就必須是0。這樣在原來P的式子中,後面乘上S與B中相應的第幾位,若是“?”,給其賦值為0。則

做三次FFT,加起來等於0的,即為匹配。

  1 //源:https://www.luogu.org/problemnew/show/P4173
  2 #include<bits/stdc++.h>
  3 using namespace std;
  4 const int maxn=1233333;
  5 const double pi=3.1415926535898;
  6 struct com
  7 {
  8     double a,b;
  9     com(double A=0,double B=0){a=A,b=B;}
 10     void operator=(com x){a=x.a,b=x.b;}
 11     com operator+(com x){return com(a+x.a,b+x.b);}
 12     com operator-(com x){return com(a-x.a,b-x.b);}
 13     com operator*(com x){return com(a*x.a-b*x.b,a*x.b+b*x.a);}
 14     com operator/(double d){return com(a/d,b/d);}
 15     com operator*(double d){return com(a*d,b*d);}
 16 }A[maxn],B[maxn],ans[maxn];
 17 int n,m,limit,r[maxn],len,g1[maxn],g2[maxn];
 18 char ch;
 19 int re(int x)
 20 {
 21     int sum=0;
 22     for(int i=0;i<len;++i)
 23     {
 24         sum=sum*2+x%2;
 25         x/=2;
 26     }
 27     return sum;
 28 }
 29 void FFT(com*A,int g)
 30 {
 31     for(int i=0;i<limit;++i)
 32         if(i<r[i])swap(A[i],A[r[i]]);
 33     for(int i=2;i<=limit;i*=2)
 34     {
 35         com w(cos(2*pi/i),g*sin(2*pi/i));
 36         for(int j=0;j<limit/i;++j)
 37         {
 38             com d(1,0);
 39             for(int k=0;k<i/2;++k)
 40             {
 41                 com a=A[i*j+k],b=d*A[i*j+i/2+k];
 42                 A[i*j+k]=a+b;
 43                 A[i*j+i/2+k]=a-b;
 44                 d=w*d;
 45             }
 46         }
 47     }
 48 }
 49 void out(com*A)
 50 {
 51     for(int i=0;i<limit;++i)cout<<A[i].a<<' ';
 52     cout<<endl;
 53 }
 54 void get(com*A,com*B)
 55 {
 56     FFT(A,1);
 57     FFT(B,1);
 58     for(int i=0;i<limit;++i)A[i]=A[i]*B[i];
 59     FFT(A,-1);
 60     for(int i=0;i<limit;++i)A[i]=A[i]/limit;
 61 }
 62 int main()
 63 {
 64     ios::sync_with_stdio(false);
 65     cin>>n>>m;
 66     for(int i=n-1;i>=0;--i)
 67     {
 68         cin>>ch;
 69         if(ch!='*')
 70         {
 71             int x=ch-'a'+1;
 72             A[i]=g1[i]=x;
 73         }
 74     }
 75     for(int i=0;i<m;++i)
 76     {
 77         cin>>ch;
 78         if(ch!='*')
 79         {
 80             int x=ch-'a'+1;
 81             g2[i]=x;
 82             B[i]=x*x*x;
 83         }
 84     }
 85     limit=1;
 86     while(limit<n+m+1)limit*=2,++len;
 87     for(int i=0;i<limit;++i)r[i]=re(i);
 88     get(A,B);
 89     for(int i=0;i<limit;++i)ans[i]=A[i];
 90     
 91     for(int i=limit-1;i>=0;--i)A[i]=g1[i]*g1[i]*g1[i];
 92     for(int i=0;i<limit;++i)B[i]=g2[i];
 93     get(A,B);
 94     for(int i=0;i<limit;++i)ans[i]=ans[i]+A[i];
 95     
 96     for(int i=limit-1;i>=0;--i)A[i]=g1[i]*g1[i];
 97     for(int i=0;i<limit;++i)B[i]=g2[i]*g2[i];
 98     get(A,B);
 99     for(int i=0;i<limit;++i)ans[i]=ans[i]-A[i]*2;
100     
101     int tot=0;
102     for(int i=n-1;i<m;++i)if(int(ans[i].a+0.5)==0)++tot;
103     cout<<tot<<endl;
104     for(int i=n-1;i<m;++i)if(int(ans[i].a+0.5)==0)cout<<i-n+2<<' ';
105     cout<<endl;
106     return 0;
107 }
程式碼

 


Prob 2

若字元只有’0'和'1'的呢?按照上面的做法,最後結果小於等於2的即為匹配(因為會有地方算兩遍)。

再擴充一下,字符集多大就做幾遍。最後的和加起來即可。

但由於一些奇妙的原因,至今我交不過去。只有網址。

其實隨便雜湊就能過了,SA也行。

https://www.luogu.org/problemnew/show/P3763


Prob 3

仍然是老套路。我們只要把其中某個陣列的長度變為兩倍,再重複寫下前面的數就行了。

類似的題目:https://www.luogu.org/problemnew/show/P3723

 

最後,如果能用一些資料結構或方法來維護的話就別寫FFT了。

相關文章