水題解析——7的倍數
題面:
【題目名稱】7的倍數
【時間限制】每個測試點300ms
【空間限制】128M
【題目描述】
給定一個各項均不相同且項數爲n的正整數數列:A1,A2,A3,……,An.從中選取若干個數(至少一個),使這些數之和爲7的倍數.求共有多少種不同的選法.
測試數據分爲兩類:
A類:數列由輸入數據直接給出;
B類:給定k(k<=100000)個正整數:p1,p2,p3,……,pk.數列由通項公式:
An=p1^n+p2^n+p3^n+……+pk^n確定.
【輸入格式】
第一行,兩個整數n,k:其中n表示數列項數.若k=0,則表示該組數據爲A類數據;若k>0,則表示該組數據爲B類數據.
第二行,若爲A類數據,則接下來有n個互不相同的正整數,其中第i個數表示數列的第i項;若爲B類數據,則接下來有k個正整數:p1,p2,p3,…,pk.(具體含義見題目描述)
【輸出格式】
一個非負整數,表示共有多少種選法.由於答案可能很大,請mod 1000000007後輸出.
【樣例輸入1】
6 0
1 2 3 4 5 6
【樣例輸出1】
9
【樣例輸入2】
4 2
3 5
【樣例輸出2】
2
【數據範圍與約定】
對於20%的數據,k=0,1<=n<=20;
對於60%的數據,k=0,1<=n<=100000;
對於另 40%的數據,1<=k<=10^5,1<=n<=10^18.
數據:
這題數據並沒有什麼特別之處,在此就不提供數據了。
題解:
20分算法:搜索
只處理A類數據.
- 很裸很裸的暴力搜索;
- 依次枚舉每個數選與不選;
- 然後驗證並計數;
- 時間複雜度O(2^n).
20分暴搜代碼:
#include<cstdio>
#define mod 1000000007
int n,k,A[1100],cnt=0;
void dfs(int p,int num)
{
if(p>n)
{
cnt+=(num%7==0);
if(cnt>mod)
{
cnt-=mod;
}
return ;
}
dfs(p+1,num);
dfs(p+1,num+A[p]);
return ;
}
int main()
{
scanf("%d%d",&n,&k);//只處理A類數據;
for(int i=1;i<=n;i++)
{
scanf("%d",&A[i]);
}
dfs(1,0);//深搜;
printf("%d",cnt-1);//排除一個也不選的情況;
return 0;
}
60分算法:揹包
在爆搜的時候,我們討論每個數選與不選的情況,這給人一種揹包問題的感覺.事實上,這還真是個揹包問題.
狀態:f[i][j]表示從前i個數中選取若干個數,使它們之和mod 7得j的方案數。
那麼我們的轉移方程是:
f[i][j]=f[i-1][j]+f[i-1][(j-(A[i]%7)+7)%7]..
後面那一坨是什麼意思?
舉個例子,比如遇到A[i]%7==4時:
那麼f[i][5]=f[i-1][5]+f[i-1][1],(注:(5-4+7)%7=8%7=1).
f[i][2]=f[i-1][2]+f[i-1][5],(注:(2-4+7)%7=5%7=5).
加7再mod 7是爲了將減得的負的餘數,轉爲正的.
當然我們還可以用滾動數組,優化空間複雜度.
具體實現參見: 60分揹包代碼:
#include<cstdio>
#define mod 1000000007
int n,k,f[2][7];
int main()
{
//仍然只處理A類數據;
scanf("%d%d",&n,&k);
f[0][0]=1;//表示餘數爲0的方案有1種;
for(int i=1,x;i<=n;i++)
{
scanf("%d",&x);
x%=7;//直接取mod
for(int j=0;j<=6;j++)
{
f[i&1][j]=f[i&1^1][(j-x+7)%7];//餘數爲j的可以由余數爲(j-x+7)%7的轉化而來;
}//因爲更新過程是一個環狀的過程,不能像裸的揹包那樣原地更新覆蓋,故採用滾動.
for(int j=0;j<=6;j++)
{
f[i&1][j]+=f[i&1^1][j];
f[i&1][j]%=mod;
}
}
printf("%d",f[n&1][0]-1);//排除一個也不選的情況;
return 0;
}
100分算法:揹包+矩陣乘法
不難發現:
- 數據很大
- 時間複雜度應該是O(log n)的,可能還會有常數
- 揹包的轉移方程只與A[i]%7的值有關,而這個總共只有7種情況
- 相同餘數對應相同的轉移方程
- 轉移方程可以寫成矩陣乘法的形式
- 對於A[i]我們只關心它們%7的值,而不關心其本身的大小
- 由費馬小定理知,A[i]%7的餘數呈週期變化,最小正週期<=6
- 根據這些我們已經可以寫出一個O(7^4 * log n)的算法了
不過還有一些細節,這些會在100分代碼中呈現:
#include<cstdio>
#define ll long long
#define mod 1000000007
ll n,r[10],xi[10],tmp[10],cnt[10];
/*
r[i]表示Ai mod 7的餘數;
xi[i]表示在p[1~k]中mod 7餘i的數的個數;
tmp[i]是一個臨時變量,用處見後文;
cnt[i]表示在n個數中,mod 7餘i的數的個數,是矩陣乘法的指數;
*/
ll A[10][10],ANS[10][10],C[10][10];
//矩陣:A--底數,ANS--答案,C--臨時中轉;
int k;
int main()
{
scanf("%I64d%d",&n,&k);
if(k==0)
{
for(int i=1,x;i<=n;i++)
{
scanf("%d",&x);
cnt[x%7]++;
}
}//A類;
else
{
for(int i=1,x;i<=k;i++)
{
scanf("%d",&x);
xi[x%7]++;
}//記錄pk數列中mod 7得i的有多少個;不記也可.
for(int i=0;i<=6;i++)
{
tmp[i]=1;
}
for(int i=1;i<=6;i++)
{
for(int j=0;j<=6;j++)
{
tmp[j]=tmp[j]*j%7;
r[i]=(r[i]+xi[j]*tmp[j])%7;
}
}//處理循環節1~6的情況;
int rr=n%6;
for(int i=1;i<=6;i++)
{
if(i<=rr)
{
cnt[r[i]]+=n/6+1;//利用循環的性質計數;
}
else
{
cnt[r[i]]+=n/6;//同上;
}
}
}
for(int i=0;i<=6;i++)
{
ANS[i][i]=1LL;
}//賦初始值;
for(int i=0;i<=6;i++)//討論各個餘數的情況;
{
for(int j=0;j<=6;j++)
{
for(int k=0;k<=6;k++)
{
A[j][k]=0;
}
}//清零底數;
for(int j=0;j<=6;j++)
{
A[j][j]+=1LL;
A[(j-i+7)%7][j]+=1LL;
}//構建矩陣;
for(;cnt[i];cnt[i]=cnt[i]>>1)
{
if(cnt[i]&1)
{
for(int s1=0;s1<=6;s1++)
{
for(int s2=0;s2<=6;s2++)
{
C[s1][s2]=0LL;
for(int s3=0;s3<=6;s3++)
{
C[s1][s2]=(C[s1][s2]+ANS[s1][s3]*A[s3][s2])%mod;
}
}
}
for(int s1=0;s1<=6;s1++)
{
for(int s2=0;s2<=6;s2++)
{
ANS[s1][s2]=C[s1][s2];
}
}
}//ANS=ANS*A%mod;
for(int s1=0;s1<=6;s1++)
{
for(int s2=0;s2<=6;s2++)
{
C[s1][s2]=0LL;
for(int s3=0;s3<=6;s3++)
{
C[s1][s2]=(C[s1][s2]+A[s1][s3]*A[s3][s2])%mod;
}
}
}
for(int s1=0;s1<=6;s1++)
{
for(int s2=0;s2<=6;s2++)
{
A[s1][s2]=C[s1][s2];
}
}//A=A*A%mod;
}//快速冪;
}
printf("%I64d",ANS[0][0]-1);//排除一個也不選的情況;
return 0;
}
至此這道水題就講完了.