1. 程式人生 > 實用技巧 >LOJ3339 「NOI2020」美食家

LOJ3339 「NOI2020」美食家

題目連結

博主有幸參加了NOI2020,考場上的經歷和心得請見這篇文章。這裡就不嘮叨了。

本題題解

樸素DP

考慮前兩檔部分分,也就是\(T\)沒那麼大的時候。我們可以做一個簡單的DP。設\(dp[i][u]\)表示在第\(i\)天,走到了圖上的節點\(u\),一路上總共能獲得的最大愉悅值。轉移時,可以列舉點\(u\)的一條出邊\((u,v,w)\),然後用\(dp[i][u]+c[v]\)去更新\(dp[i+w][v]\),當然,如果第\(i+w\)天點\(v\)恰好在舉辦美食節,還要加上額外產生的愉悅值。

因為保證了\(w>0\),所以這個DP滿足無後效性,狀態非常合理。時間複雜度\(O(T(n+m))\)

,期望得分\(40\)分。加上環的部分分(簡單特判),可以得到\(50\)分。

矩陣優化

因為\(T\)很大而\(n,m,w\)都較小,容易想到用矩陣快速冪優化。

對於傳統的矩陣乘法,我們是這樣定義的:

\[C=A\times B \\\Leftrightarrow C[i][j] = \sum_{k=1}^{n}A[i][k]\times B[k][j] \]

但在本題中,DP的轉移不是相加而是取\(\max\)。我們根據本題中的需要,定義一種新的矩陣乘法,不妨記為\(\otimes\)

\[C=A\otimes B\\ \Leftrightarrow C[i][j] = \max_{k=1}^{n}\{A[i][k]+B[k][j]\} \]

也就是把原來外層的\(+\)換成\(\max\),把原來內層的\(\times\)換成\(+\)。可以證明,這個矩陣乘法仍然是成立的,並且滿足原來的種種性質(比如結合律,其實矩陣快速冪優化DP的本質就是用到結合律)。證明略。

值得一提的是,這種重新定義的矩陣乘法,在一類動態DP問題中經常用到。例如NOIP2018 保衛王國


回到我們的DP。先只考慮\(k=0\),也就是沒有美食節的情況。

發現從\(dp[i][\dots]\)轉移到\(dp[i+w][\dots]\)不太好處理(一般的矩陣快速冪優化DP,只會從\(i\)轉移到\(i+1\))。但是我們發現\(w\)很小,\(\leq 5\),所以可以考慮把原來的一條邊,拆成\(w\)

條邊。也就是原本的\((u,v,w)\),拆成:\((u,e_1,w),(e_1,e_2,0),\dots,(e_{w-2},e_{w-1},0),(e_{w-1},v,0)\)。這樣雖然點數變多了(變成了\(n+4m\)),但是隻會從\(i\)轉移到\(i+1\)了,可以用矩陣快速冪優化DP。

具體來說,我們根據兩個點之間相連的邊權(沒有邊就是\(-\inf\),有邊邊權就是\(0\)\(w\),前面已經標出),構造一個轉移矩陣\(G\)。則\(dp[i]=dp[i-1]\otimes G\)。答案就是\(dp[T][1] = (dp[1]\otimes G^T)[1]\)

時間複雜度\(O((n+4m)^3\log T)\)

注意到\(m\)可能比\(n\)大不少,所以可以把拆邊改成拆點。具體來說,把一個點\(u\),變成\(u_1\to u_2\to \dots \to u_5\)的這樣一個結構,邊權都是\(0\)。出發點設為\(1_1\)。對於一條邊\((u,v,w)\),我們從\(u_w\)\(v_1\)連一條邊權為\(c[v]\)的邊。這樣相當於要先從\(u_1\)走到\(u_w\),再從\(u_w\)走到\(v\),剛好經過了\(w\)條邊,也就是起到了從\(dp[i]\)轉移到\(dp[i+w]\)的效果。

時間複雜度優化為\(O((5n)^3\log T)\)。可以通過\(k=0\)的部分分。結合前面的暴力,期望得分\(65\)分。

k個美食節怎麼處理?

以上的矩陣快速冪,目前還只能處理\(k=0\),也就是沒有美食節的情況。考慮\(k>0\)時怎麼做。

注意到題目保證了任意兩個美食節不在同一天舉辦。所以可以先將美食節按舉辦時間從小到大排序。然後在任意兩個美食節之間,做一遍普通的DP轉移。例如,第\(j\)個美食節的舉辦日期為\(t_j\),第\(j-1\)個美食節舉辦日前為\(t_{j-1}\),則這段的轉移就是:\(dp[t_{j}] = dp[t_{j-1}]\otimes G^{t_{j}-t_{j-1}}\)。轉移完成後,再令\(dp[t_j][x_j]\texttt{+=}y_j\)。也就是加上了美食節的貢獻。

最後,如果\(t_k\neq T\),再從\(dp[t_k]\)轉移到\(dp[T]\)就行。

直接按此做法實現的話,時間複雜度\(O(k\cdot (5n)^3\cdot \log T)\)。可以通過\(k\leq 10\)的部分分,結合前面的暴力,期望得分\(75\)分。

進一步優化

我們發現所做的\(k\)次轉移,每次都是乘以同一個轉移矩陣的若干次冪。

我們考慮把【\(G\)的【\(2\)的次冪】次冪】(也就是\(G^1,G^2,G^4,G^8,G^{16},\dots ,G^{2^{29}}\))預處理出來。預處理的時間複雜度為\(O((5n)^3\log T)\)

然後,在做每個美食節的轉移時,對\((t_j-t_{j-1})\)這個數做二進位制分解,設\(t_j-t_{j-1}=2^{p_1}+2^{p_2}+\dots +2^{p_l}\),也就是\(p_1\dots p_l\)這些二進位制位上為\(1\)。那麼我們只需要用\(dp[t_{j-1}]\),依次乘以預處理好的\(G^{2^{p_1}},G^{2^{p_2}},\dots,G^{2^{p_l}}\),即可得到\(dp[t_j]\)。而因為\(dp[i]\)是一個\(1\times 5n\)的“長條”(又叫向量)而不是一個真正的矩陣,所以每次乘法的時間複雜度只有\(O((5n)^2)\)。所有轉移的總複雜度\(O(k\cdot (5n)^2\cdot \log T)\)

總時間複雜度\(O((5n)^3\log T+ k\cdot (5n)^2\log T)\)

這個優化方法和NOI Online 3 魔法值這題很類似。

參考程式碼

在LOJ檢視

友情提醒,在LOJ提交時,記得開freopen,也就是和考場上要求一樣。

//problem:LOJ3339
#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;

template<typename T>inline void ckmax(T& x,T y){x=(y>x?y:x);}
template<typename T>inline void ckmin(T& x,T y){x=(y<x?y:x);}

const int MAXN = 50, MAXM = 501, MAXW = 5, MAXK = 200;
const int LOGT = 29;
const int MT_SIZE = MAXN*5;
const ll LL_INF = 1e18;

struct Matrix{
	ll a[MT_SIZE+5][MT_SIZE+5];
	int size;
	void identity() {
		for(int i=1;i<=size;++i) {
			for(int j=1;j<=size;++j){
				a[i][j] = (i==j ? 0 : -LL_INF);
			}
		}
	}
	Matrix(){
		for(int i=1;i<=MT_SIZE;++i)
			for(int j=1;j<=MT_SIZE;++j)
				a[i][j] = -LL_INF;
		size=0;
	}
};
Matrix operator * (const Matrix& A, const Matrix& B) {
	Matrix C;
	assert(A.size==B.size);
	C.size = A.size;
	for(int i=1;i<=A.size;++i){
		for(int j=1;j<=A.size;++j){
			for(int k=1;k<=A.size;++k){
				if(A.a[i][k] == -LL_INF || B.a[k][j] == -LL_INF)
					continue;
				ckmax(C.a[i][j], A.a[i][k]+B.a[k][j]);
			}
		}
	}
	return C;
}
Matrix mat_pow(Matrix X,int i) {
	Matrix Y;
	Y.size = X.size;
	Y.identity();
	while(i){
		if(i&1)
			Y=Y*X;
		X=X*X;
		i>>=1;
	}
	return Y;
}

int n,m,T,K,c[MAXN+5];
int id[MAXN+5][MAXW+5],cnt_id;

struct Event{
	int t,u,w;
	bool operator < (const Event& rhs) const {
		return t < rhs.t;
	}
}ev[MAXK+5];

Matrix trans,pow_of_trans[LOGT+1];
void init_pow_of_trans() {
	pow_of_trans[0] = trans;
	for(int i=1; i<=LOGT; ++i) {
		pow_of_trans[i] = pow_of_trans[i-1] * pow_of_trans[i-1];
	}
}
void mul_pow_of_trans(Matrix& A, int mi) {
	// O(size^2 * log k)
	for(int bit=0; bit<=LOGT; ++bit) {
		if((mi>>bit) & 1) {
			//向量乘矩陣
			Matrix res;
			res.size=A.size;
			for(int j=1; j<=A.size; ++j) {
				for(int k=1; k<=A.size; ++k) {
					if(A.a[1][k] == -LL_INF || pow_of_trans[bit].a[k][j] == -LL_INF)
						continue;
					ckmax(res.a[1][j], A.a[1][k] + pow_of_trans[bit].a[k][j]);
				}
			}
			A = res;
		}
	}
}

int main() {
//	freopen("delicacy.in", "r", stdin);
//	freopen("delicacy.out", "w", stdout);
	cin >> n >> m >> T >> K;
	for(int i=1;i<=n;++i) {
		cin >> c[i];
	}
	trans.size = n*5;
	for(int i=1;i<=n;++i) {
		for(int j=1;j<=5;++j) {
			id[i][j] = ++cnt_id;
		}
		for(int j=1;j<5;++j) {
			trans.a[id[i][j]][id[i][j+1]] = 0;
		}
	}
	for(int i=1;i<=m;++i) {
		int u,v,w;
		cin >> u >> v >> w;
		trans.a[id[u][w]][id[v][1]]=c[v];
	}
	for(int i=1; i<=K; ++i) {
		cin >> ev[i].t >> ev[i].u >> ev[i].w;
	}
	sort(ev+1, ev+K+1);
	
	Matrix A;
	A.size = n*5;
	A.a[1][id[1][1]] = c[1];
	init_pow_of_trans();
	
	int last_time = 0;
	for(int i=1; i<=K; ++i) {
		mul_pow_of_trans(A, ev[i].t - last_time);
		// 相當於: A = A * mat_pow(trans, ev[i].t - last_time);
		if(A.a[1][id[ev[i].u][1]] != -LL_INF) {
			A.a[1][id[ev[i].u][1]] += ev[i].w;
		}
		last_time = ev[i].t;
	}
	if(last_time != T) {
		mul_pow_of_trans(A, T - last_time);
	}
	if(A.a[1][id[1][1]] == -LL_INF)
		cout << -1 << endl;
	else
		cout << A.a[1][id[1][1]] << endl;
	return 0;
}