【2020省選Day2T1】 LOJ3302 「聯合省選 2020 A | B」信號傳遞


題目鏈接

初步轉化

按題意,我們暴力枚舉這\(m\)個信號站的排列順序,時間復雜度\(O((m!)\cdot n)\)

容易發現,題目給出的這個長度為\(n\)的序列\(S\)具體是什么不重要,重要的是,每一對信號站\(i,j\),在\(S\)里作為相鄰的位置,出現了多少次。也就是有多少個位置\(p\) (\(1\leq p<n\))滿足\(S_p=i,S_{p+1}=j\)。我們記這個數量為\(\text{cnt}[i][j]\)

對於任意\(i\neq j\)\(\text{cnt}[i][j]\)就表示要從信號站\(i\),向信號站\(j\),進行多少次傳遞。設兩個信號站的位置,分別為\(\text{pos}[i],\text{pos}[j]\),則它們會對答案產生的代價就是:

\[\begin{cases} \text{pos}[j]-\text{pos}[i]&&(\text{pos}[i]<\text{pos}[j])\\ k\cdot(\text{pos}[i]+\text{pos}[j])&&(\text{pos[i]}>\text{pos}[j]) \end{cases} \]

做了這步轉化后,如果還是暴力枚舉這\(m\)個信號站的排列順序,時間復雜度優化為\(O(n+(m!)\cdot m^2)\)。至此我們復雜度的瓶頸與\(n\)無關了。

朴素DP

接下來,結合\(m\)的大小,容易想到狀壓DP。此處就有兩種狀態設計的思路:

  • \(dp[s]\)表示考慮了前\(|s|\)個數,放在了\(s\)里的這些位置上。
  • \(dp[s]\)表示考慮了前\(|s|\)個位置,填了\(s\)里的這些數。

發現,第一種狀態設計是無法轉移的。所以我們選擇第二種。事實上,考試時我就一直在想第一種,把自己搞自閉了。其實,按位置考慮、按數值考慮,這都是常見的方法,所以要學會靈活變通,一個不行就想想另一個。

我們選擇了第二種狀態設計:設\(dp[s]\)表示考慮了前\(|s|\)個位置,填了\(s\)里的這些數。

那考慮轉移。首先要枚舉在新加入的位置,也就是在第\(\text{pos}=|s|+1\)個位置,填哪個數。假設我們填\(i\)。考慮填\(i\)會新增哪些代價。它會分別和:前面的數(也就是\(s\)里的數)、后面的數(也就是不在\(s\)里,但也不等於\(i\)的數)產生代價。而且每種代價都有兩個方向。具體來說:

  • 對於一個前面的數\(j\),從\(i\)\(j\)產生的代價是:\(\text{pos}\cdot k\cdot \text{cnt}[i][j]\)
  • 對於一個前面的數\(j\),從\(j\)\(i\)產生的代價是:\(\text{pos}\cdot\text{cnt}[j][i]\)
  • 對於一個后面的數\(j\),從\(i\)\(j\)產生的代價是:\(-\text{pos}\cdot \text{cnt}[i][j]\)
  • 對於一個后面的數\(j\),從\(j\)\(i\)產生的代價是:\(\text{pos}\cdot k\cdot \text{cnt}[j][i]\)

所以我們也可以寫出式子:

\[dp[s+i]\leftarrow dp[s]+\text{pos}\cdot \sum_{j\in s}(k\cdot\text{cnt}[i][j]+\text{cnt}[j][i])+\text{pos}\cdot\sum_{j\notin(s+i)}(-\text{cnt}[i][j]+k\cdot \text{cnt}[j][i]) \]

如果枚舉\(i\),再枚舉\(j\),DP的時間復雜度\(O(2^mm^2)\)。期望得70分。

優化時間

我們繼續優化。考慮只枚舉\(i\)。把\(\text{pos}\)前的系數(也就是所有\(j\)的貢獻之和),預處理出來,不妨記為:\(\text{cost}(s,i)\)。那么上述的轉移式,也可以改寫為:\(dp[s+i]\leftarrow dp[s]+\text{pos}\cdot \text{cost}(s,i)\)

考慮預處理\(\text{cost}(s,i)\)。首先,根據定義,\(i\notin s\)

我們考慮,\(\text{cost}(s,i)\),可以從“\(s\)去掉某個數”的狀態,轉移過來。我們不妨就去掉\(j=\text{lowbit}(s)\)(也就是二進制下最低的,為\(1\)的位)。那么,\(\text{cost}(s,i)=\text{cost}(s-j,i)+(k\cdot \text{cnt}[i][j]+\text{cnt}[j][i])-(-\text{cnt}[i][j]+k\cdot \text{cnt}[j][i])\)

這樣轉移是\(O(1)\)的。所以預處理的時間復雜度為\(O(2^mm)\),DP的時間復雜度也降為\(O(2^mm)\)。總時間復雜度\(O(n+2^mm)\)。但是\(\text{cost}\)數組會占用\(O(2^mm)\)的空間,這無法承受。所以還要繼續優化空間。

優化空間

這題優化空間的方法很多。我講一種比較好想的。想了解更多方法,可以閱讀這篇文章

發現\(\text{cost}\)\(\text{dp}\)的轉移,都是按集合從小到大進行的。所以我們不妨就按這個順序,一邊DP,一邊求\(\text{cost}\)

對每個\(s\),我們把\(\text{cost}(s,\dots)\)視為一個大小為\(m\)的數組。當前的\(s\),我們先做DP轉移,再拿\(\text{cost}(s\dots)\)數組去更新所有\(\text{cost}(s'\dots )\)。發現更新完之后,\(\text{cost}(s,\dots)\)這個大小為\(m\)的數組,就可以不要了!

如果能及時地把\(\text{cost}(s,\dots)\)這個數組,以某種方式“刪除”掉(不再占用空間)。那么,在整個DP的過程中,同一時刻,我們存儲的最多只有兩種大小\(s\)。因為\(\text{cost}(s,\dots)\)只會轉移到比它恰好大\(1\)\(s'\)。那么空間的極限,就是\({23\choose 12}+{23\choose 11}\)個大小為\(m\)的數組,約等於\(237\text{MB}\),完全可以了。

那么怎么實現這個“刪除”呢?一種方法是用滾動數組。按大小枚舉所有集合。每處理完一種大小,就把數組“滾一次”。另一種方法,是直接利用\(\texttt{stl}\)里的\(\texttt{queue}\),向BFS一樣,每次取出隊首的\(s\)\(\texttt{pop}\)掉就行。

空間復雜度,\(\displaystyle O\left(2^m+m\cdot{m\choose\frac{m}{2}}\right)\)。前面分析過,大約\(237\text{MB}\),可以通過本題。

參考代碼(在LOJ查看):

//problem:LOJ3302
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

inline void ckmin(int& x,int y){x=(x<y?x:y);}

const int MAXN=1e5,MAXM=23;
const int INF=1e9;
int n,m,K,arr[MAXN+5],cnt[MAXM][MAXM];
int id[1<<MAXM],bitcnt[1<<MAXM];
int dp[1<<MAXM];

struct State{
	int s;
	int v[MAXM];
};

int main() {
	//freopen("transfer.in","r",stdin);
	//freopen("transfer.out","w",stdout);
	cin>>n>>m>>K;
	for(int i=1;i<=n;++i){
		cin>>arr[i];
		arr[i]--;
	}
	for(int i=1;i<n;++i){
		cnt[arr[i]][arr[i+1]]++;
	}
	
	queue<State>q;
	State s;
	s.s=0;
	for(int i=0;i<m;++i){
		s.v[i]=0;
		for(int j=0;j<m;++j)if(j!=i){
			s.v[i]-=cnt[i][j];
			s.v[i]+=K*cnt[j][i];
		}
	}
	q.push(s);
	
	int S=(1<<m)-1;
	for(int i=0;i<m;++i)id[1<<i]=i;
	for(int i=1;i<=S;++i){
		bitcnt[i]=bitcnt[i>>1]+(i&1);
		dp[i]=INF;
	}
	while(!q.empty()){
		State curs=q.front();q.pop();
		assert(dp[curs.s]<INF);
		int p=bitcnt[curs.s]+1;
		for(int t=S^curs.s;t;t-=(t&(-t))){
			//枚舉curs.s的補集里的一個點i
			int i=id[t&(-t)];
			int new_dp=dp[curs.s];
			int new_s=(curs.s|(1<<i));
			new_dp+=p*curs.v[i];
			
			ckmin(dp[new_s],new_dp);
		}
		for(int i=0;i<m;++i){
			if((curs.s>>i)&1)break;
			State new_state;
			new_state.s=(curs.s|(1<<i));
			for(int t=S^new_state.s;t;t-=(t&(-t))){
				//枚舉new_state.s的補集里的一個點j 
				int j=id[t&(-t)];
				new_state.v[j]=curs.v[j]
					+(K*cnt[j][i]+cnt[i][j])
					-(-cnt[j][i]+K*cnt[i][j]);
			}
			q.push(new_state);
		}
	}
	cout<<dp[S]<<endl;
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM