SP10606 Balanced Numbers
關於最佳化方式的說明詳見數位dp例題及詳解-下。
SPOJ註冊不上所以暫時無法提交w,但是3份程式碼與正解對拍沒有問題。
使用\(vis[0\sim 9]\)表示\(0\sim 9\)的訪問情況,\(sta[0\sim 9]\)表示\(0\sim 9\)填寫個數的奇偶性(奇數為\(1\),偶數為\(0\))。暴搜先打出來,然後考慮怎麼記憶化。我們發現如果兩個狀態(\(limit=false\))填寫到同一位置\(pos\),而且\(vis\)和\(sta\)都相同,那麼這兩個狀態答案相同。
所以用\(f[pos][vis][sta]\)來記憶化,空間\(20*1024*1024\),不會MLE(1.46G的記憶體)
注意到資料範圍,可能需要開unsigned long long
,注意這樣\(f\)陣列就不能初始化為\(-1\)了,可以再開一個bool
型別的\(fv\)表示\(f\)的這個狀態是否計算出答案了。
1.1 Code
#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
int t,l,r,a[30];
bitset<10> vis,sta;
bool fv[30][1024][1024];
int f[30][1024][1024];
int dfs(int pos,bool limit,bool zero){
if(pos==0){
for(int i=0;i<=9;i++) if(vis[i]&&sta[i]==i%2) return 0;
return 1;
}
int numvis=vis.to_ullong(),numsta=sta.to_ullong();
if(!limit&&!zero&&fv[pos][numvis][numsta])
return f[pos][numvis][numsta];
int rig=limit?a[pos]:9,ans=0;
for(int i=0;i<=rig;i++){
bool is=(zero&&i==0);
bool tvis=vis[i],tsta=sta[i];
if(!is) vis[i]=1,sta[i]=!sta[i];
ans+=dfs(pos-1,limit&&i==rig,is);
vis[i]=tvis,sta[i]=tsta;
}
if(!limit&&!zero) f[pos][numvis][numsta]=ans,fv[pos][numvis][numsta]=1;
return ans;
}
int solve(int x){
int len=0;
while(x){
a[++len]=x%10;
x/=10;
}
return dfs(len,1,1);
}
signed main(){
memset(fv,0,sizeof fv);
cin>>t;
while(t--){
cin>>l>>r;
cout<<solve(r)-solve(l-1)<<endl;
}
return 0;
}
空間最佳化
(第3種最佳化方式)
其實上面的就能過了,但是我們注意到還有最佳化空間。
上面的表示其實就是四進位制,但是我們發現\(vis[i]=0,sta[i]=1\)的情況不存在,所以我們可以最佳化成三進位制,狀壓一下就可以了。總空間\(20*(3^{10})=20*59049\)。
按道理說應該只是最佳化了空間而沒有最佳化時間,因為上面所說的情況根本不會搜尋到。
(但很奇怪的是這份程式碼跑得奇快,具體見下面的時間對比,如果大家有解答請在評論區告訴我,謝謝!)
1.2 空間最佳化Code
#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
int t,l,r,a[30];
int sta[10];
bool fv[30][59049];
int f[30][59049];
//0沒訪問,1訪問奇數次,2訪問偶數次,10位三進位制
//最佳化掉了“沒訪問過,奇數次”的狀態
int to_num(){
int ans=0;
for(int i=0;i<=9;i++) ans=ans*3+sta[i];
return ans;
}
int dfs(int pos,bool limit,bool zero){
if(pos==0){
for(int i=0;i<=9;i++){
if(sta[i]==0) continue;
if(sta[i]-1!=i%2) return 0;
}
return 1;
}
int numsta=to_num();
if(!limit&&!zero&&fv[pos][numsta])
return f[pos][numsta];
int rig=limit?a[pos]:9,ans=0;
for(int i=0;i<=rig;i++){
bool is=(zero&&i==0);
int tsta=sta[i];
if(!is) sta[i]=(sta[i]==0||sta[i]==2)?1:2;
ans+=dfs(pos-1,limit&&i==rig,is);
sta[i]=tsta;
}
if(!limit&&!zero) f[pos][numsta]=ans,fv[pos][numsta]=1;
return ans;
}
int solve(int x){
int len=0;
while(x){
a[++len]=x%10;
x/=10;
}
return dfs(len,1,1);
}
signed main(){
memset(fv,0,sizeof fv);
cin>>t;
while(t--){
cin>>l>>r;
cout<<solve(r)-solve(l-1)<<endl;
}
return 0;
}
究極時空最佳化
(第1種最佳化方式)
結論:只要vis奇數位上1的個數
、vis偶數位上1的個數
、sta奇數位上1的個數
、sta偶數位上1的個數
都分別相等,兩種狀態答案就一樣。所以可以直接使用\(f[pos][a][b][c][d]\)來記憶化,也可以直接壓縮成\(f[pos][a]\)。空間\(20*(6^4)=20*1296\),時間也是。
為什麼呢?如果你一共訪問了\(n\)個奇數,其中有\(m(m\leq n)\)個奇數訪問了奇數次。那麼不用管具體這些奇數是幾,因為結果中奇數互相換是不會影響的。比如\(331132377\)滿足條件,那麼我把\(1\)和\(7\)互換,或者把\(3\)都換成\(9\)……都不會影響結果。偶數同理。
1.3 究極時空最佳化Code
#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
int t,l,r,a[30];
bitset<10> vis,sta;
bool fv[30][1296];
int f[30][1296];
int dfs(int pos,bool limit,bool zero){
if(pos==0){
for(int i=0;i<=9;i++) if(vis[i]&&sta[i]==i%2) return 0;
return 1;
}
int num=(vis[0]+vis[2]+vis[4]+vis[6]+vis[8]);
num=num*6+(vis[1]+vis[3]+vis[5]+vis[7]+vis[9]);
num=num*6+(sta[0]+sta[2]+sta[4]+sta[6]+sta[8]);
num=num*6+(sta[1]+sta[3]+sta[5]+sta[7]+sta[9]);
if(!limit&&!zero&&fv[pos][num])
return f[pos][num];
int rig=limit?a[pos]:9,ans=0;
for(int i=0;i<=rig;i++){
bool is=(zero&&i==0);
bool tvis=vis[i],tsta=sta[i];
if(!is) vis[i]=1,sta[i]=!sta[i];
ans+=dfs(pos-1,limit&&i==rig,is);
vis[i]=tvis,sta[i]=tsta;
}
if(!limit&&!zero){
f[pos][num]=ans,fv[pos][num]=1;
}
return ans;
}
int solve(int x){
int len=0;
while(x){
a[++len]=x%10;
x/=10;
}
return dfs(len,1,1);
}
signed main(){
memset(fv,0,sizeof fv);
cin>>t;
while(t--){
cin>>l>>r;
cout<<solve(r)-solve(l-1)<<endl;
}
return 0;
}
執行消耗對比
從上到下分別是洛谷題解排名第一、樸素演算法、空間最佳化、究極時空最佳化的程式碼的執行消耗,每個樣例測試點均在\(5\)個以內。所以可以看出,樸素演算法即可透過此題,而最佳化後的程式碼,無論在時間還是空間方面,均比題解優。