提高組雜題訓練1

XiaoLe_MC發表於2024-10-12

A [USACO22DEC] Breakdown P

首先 \(N\le300\) \(k\le8\) 看樣子複雜度是個 3 次的東西。一些套路的東西比如刪邊改加邊不說了。這個 \(K\le8\) 很有講究。

首先,不妨折半一下,算出從 1 經過一半條邊到 \(u\) 的最短路徑和 \(u\)\(n\) 的最短路徑,那麼答案就可以 \(\mathcal{O}(n)\) 合併。對於 \(k'\le 4\) ,可以維護兩個陣列 \(f[u][v]\)\(g[u][v]\),分別表示 \(u\sim v\) 走一條邊和走兩條邊的最短路,維護 \(h[u]\) 陣列表示從起始節點到 \(u\) 經過 \(k'\) 條邊的最短路徑。可以驚人的發現 1 和 2 可以湊出所有 4 以內的情況!!當加入邊 \(u, v\) 時,當且僅當 \(u\)\(bg\) 的時候需要列舉所有斷點維護 \(h\),所以加邊操作均攤 \(\mathcal{O}(n)\)。總複雜度 \(\mathcal{O}(N^3)\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
constexpr int N = 305;
int n, k, x[N*N], y[N*N], ans[N*N], edge[N][N];
struct XiaoLe{
	int f[N][N], g[N][N], h[N], fk, bg;
	const int& operator [](const int x) const { return h[x]; }
	inline void build(int kf, int gb){
		fk = kf; bg = gb;
		memset(f, 0x3f, sizeof(f));
		if(fk > 1) memset(g, 0x3f, sizeof(g));
		memset(h, 0x3f, sizeof(int)*(n+1));
		if(!fk) h[bg] = 0;
	}
	inline void add(int u, int v, int val){
		if(!fk) return;
		if(fk == 1){
			if(u ^ bg) return;
			h[v] = min(h[v], val);
			return;
		}
		f[u][v] = val;
		for(int i=1; i<=n; ++i){
			g[i][v] = min(g[i][v], f[i][u] + val);
			g[u][i] = min(g[u][i], val + f[v][i]);
		}
		if(fk == 2){
			for(int i=1; i<=n; ++i)
				h[i] = min(h[i], g[bg][i]);
		} else if(fk == 3){
			if(u == bg){
				for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j)
					h[i] = min(h[i], f[bg][j] + g[j][i]);
			} else{
				for(int i=1; i<=n; ++i){
					h[i] = min({h[i], g[bg][u] + f[u][i], g[bg][v] + f[v][i]});
					h[u] = min(h[u], f[bg][i] + g[i][u]);
					h[v] = min(h[v], f[bg][i] + g[i][v]);
				}
					
			}
		} else{
			if(u == bg){
				for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j)
					h[i] = min(h[i], g[bg][j] + g[j][i]);
			} else {
				for(int i=1; i<=n; ++i){
					h[i] = min({h[i], g[bg][u] + g[u][i], g[bg][v] + g[v][i]});
					h[u] = min(h[u], g[bg][i] + g[i][u]);
					h[v] = min(h[v], g[bg][i] + g[i][v]);
				}
			}
		}
	}
} m1, mn;
signed main(){
	ios::sync_with_stdio(0), cout.tie(0), cout.tie(0);
	cin>>n>>k; int nn = n * n;
	for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j)
		cin>>edge[i][j];
	m1.build(k >> 1, 1), mn.build((k + 1) >> 1, n); 
	memset(ans, 0x3f, sizeof(int)*(nn+1));
	for(int i=1; i<=nn; ++i) cin>>x[i]>>y[i];
	for(int i=nn, u, v; i>=1; --i){
		for(int j=1; j<=n; ++j)
			ans[i] = min(ans[i], m1[j] + mn[j]);
		ans[i] = ans[i] > 1e9 ? -1 : ans[i];
		u = x[i], v = y[i];
		m1.add(u, v, edge[u][v]);
		mn.add(v, u, edge[u][v]);
	}
	for(int i=1; i<=nn; ++i) cout<<ans[i]<<'\n';
	return 0;
}

B [USACO22DEC] Making Friends P

按時間模擬,對每個節點維護 set 表示當前節點都和誰是朋友關係,每次刪掉一個節點之後,ans 就加上 size,並把新建立的朋友關係加到下一個最先走掉的節點的 set 裡。相當於維護一個集合,也可以使用線段樹合併。最後 ans -= m。

#include<bits/stdc++.h>
using namespace std;
constexpr int N = 2e5 + 5;
int n, m; long long ans;
set<int> st[N];
int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>n>>m; ans = -m;
	for(int i=1, u, v; i<=m; ++i){
		cin>>u>>v; if(u > v) swap(u, v);
		st[u].insert(v);
	}
	for(int i=1; i<=n; ++i){
		if(st[i].empty()) continue;
		ans += st[i].size();
		int u = *st[i].begin(), v = i;
		st[i].erase(st[i].begin());
		if(st[u].size() < st[v].size()) swap(st[u], st[v]);
		for(int it : st[v]) st[u].insert(it);
	} return cout<<ans<<'\n', 0;
}

C [USACO22DEC] Palindromes P

分析一下回文串的性質,因為只有兩種字元,所以很好分析,這兩種字元就用 \(0\) \(1\) 代替了,並且只要 \(1\) 滿足性質,\(0\) 必然滿足,所以只分析 \(1\) 即可。

對於一個串 \([L,R]\) ,它是軸對稱的,並且對稱中心為一個或者兩個 \(1\),不妨列舉所有對稱中心並擴充套件。假如擴充套件的兩個 \(1\) 分別為 \(l\)\(r\),那麼這倆關於對稱中心的充要條件即為 \(l+r=L+R\),那麼移動步數為 \(|L+R-(l+r)|\)。那麼每次擴充套件兩個 \(1\),就把它的權值加到樹狀陣列裡維護即可。複雜度 \(\mathcal{O}(N^2\log N)\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
constexpr int N = 7505;
string s; int n, ans, g[N], cnt;
struct BIT{
	#define lb ((i) & (-i))
	int t[N<<1];
	inline void add(int x, int val){
		for(int i=x; i<=n*2; i+=lb) t[i] += val;
	}
	inline int qry(int x){
		int ans = 0;
		for(int i=x; i; i-=lb) ans += t[i];
		return ans;
	}
} t1, t2;
signed main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>s; n = s.size();
	for(int i=1; i<=n; ++i) if(s[i-1] == 'G') g[++cnt] = i;
	g[cnt+1] = n+1;
	for(int i=1, x=1, y=1; i<=cnt; ++i, x=i, y=i){
		int sum = 0, nm = 0;
		while(1 <= x && y <= cnt){
			int tot = g[x] + g[y];
			if(x != i) t1.add(tot, tot), t2.add(tot, 1), sum += tot, ++nm;
			for(int l=g[x]; l>g[x-1]; --l) for(int r=g[y]; r<g[y+1]; ++r){
				if((r - l) & 1){ --ans; continue; }
				tot = l + r;
				int val = t1.qry(tot-1), num = t2.qry(tot-1);
				ans += llabs((tot >> 1) - g[i]);
				ans += num * tot - val + sum - val - tot * (nm - num);
			} --x, ++y;
		}
        memset(t1.t, 0, sizeof(int) * (2*n + 1));
        memset(t2.t, 0, sizeof(int) * (2*n + 1));
	}
    for(int i=1, x=1, y=2; i<cnt; ++i, x=i, y=i+1){
        int sum = 0, nm = 0;
        while(1 <= x && y <= cnt){
            int tot = g[x] + g[y];
            t1.add(tot, tot), t2.add(tot, 1), sum += tot, ++nm;
            for(int l=g[x]; l>g[x-1]; --l) for(int r=g[y]; r<g[y+1]; ++r){
                tot = l + r;
                int val = t1.qry(tot-1), num = t2.qry(tot-1);
                ans += (num * tot - val) + sum - val - tot * (nm - num);
            } --x; ++y;
        }
        memset(t1.t, 0, sizeof(int) * (2*n + 1));
        memset(t2.t, 0, sizeof(int) * (2*n + 1));
    } return cout<<ans, 0;
}

D [USACO23JAN] Tractor Paths P

神一樣的倍增,屎一樣的題面,對著題目瞪了有半個多小時……

不會倍增,看了題解,太™強拉!!!

首先,這些區間的左端點和右端點單調遞增,所以如果想知道一個區間最遠能到達哪裡,就貪心地往最右邊走就好了。所以可以使用倍增最佳化此過程。這是第一個答案求法。

對於第二問,維護特殊拖拉機的字首和的倍增陣列,沿著你倍增過來的路線加貢獻即可。

#include<bits/stdc++.h>
using namespace std;
#define p pair<int, int>
constexpr int N = 2e5 + 5;
int n, m, L[N], R[N], lt[20][N], rt[20][N], sl[20][N], sr[20][N], sum[N];
string s1, s2;
inline int Dis(int u, int v){
	if(u == v) return 0;
	int dis = 0;
	for(int i=__lg(n); i>=0; --i) if(lt[i][u] != -1 && lt[i][u] < v)
		dis |= (1<<i), u = lt[i][u];
	return dis + 1;
}
int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>n>>m>>s1>>s2;
	for(int i=1, cnt=0, now=1; i<=n<<1; ++i){
		if(s1[i-1] == 'L') ++cnt;
		else R[now++] = cnt; 
	}
	for(int i=n<<1, cnt=n+1, now=n; i>=1; --i){
		if(s1[i-1] == 'R') --cnt;
		else L[now--] = cnt;
	}
	for(int i=1; i<=n; ++i) sum[i] = sum[i-1] + s2[i-1] - '0';
	memset(lt, 0xff, sizeof(lt)); memset(rt, 0xff, sizeof(rt));
	for(int i=1; i<=n; ++i) lt[0][i] = R[i], sl[0][i] = sum[R[i]];
	for(int i=1; i<=n; ++i) rt[0][i] = L[i], sr[0][i] = sum[L[i]-1];
	for(int i=1; i<=__lg(n); ++i) for(int j=1; j<n; ++j) if(lt[i-1][j] > 0){
		lt[i][j] = lt[i-1][lt[i-1][j]];
		if(lt[i][j] > 0) sl[i][j] = sl[i-1][j] + sl[i-1][lt[i-1][j]];
	}
	for(int i=1; i<=__lg(n); ++i) for(int j=n; j>1; --j) if(rt[i-1][j] > 0){
		rt[i][j] = rt[i-1][rt[i-1][j]];
		if(rt[i][j] > 0) sr[i][j] = sr[i-1][j] + sr[i-1][rt[i-1][j]];
	}
	while(m--){
		int u, v, dis; cin>>u>>v;
		cout<<(dis = Dis(u, v))<<' '; --dis;
		int ans = s2[u-1] - '0' + s2[v-1] - '0';
		if(u == v){ cout<<ans; continue; }
		for(int i=__lg(n); i>=0; --i) if(dis & (1<<i))
			ans += sl[i][u], u = lt[i][u];
		for(int i=__lg(n); i>=0; --i) if(dis & (1<<i))
			ans -= sr[i][v], v = rt[i][v];
		cout<<ans<<'\n';
	} return 0;
}

E [USACO23JAN] Mana Collection P

很巧妙的一道題

首先可以發現,當走過的點集確定之後,答案的大小僅與每個節點最後走到的時間有關,即為(\(d_i\) 為最後遍歷的時間):

\[ans=\sum_{i=1}^{k}val_i*d_i \]

而貪心的說,對於每個節點的最後遍歷時間越大,答案也就越大。一個節點的最短最後遍歷時間即為 \(d_i=s-\sum_\limits{j=i}^{k-1}dis_{j,j+1}\),其中 \(dis\) 為兩點間最短路徑。那麼帶入剛才的獅子可得:

\[ans = s{\large \sum_{i = 1}^{k}} val[i]-{\large \sum_{i = 1}^{k-1}}dis(c_i, c_{i+1})\sum_{j = 1}^{i}val[j] \]

右邊那一坨拿狀壓爆搞即可。

維護凸包或者使用李超線段樹即可。複雜度 \(\mathcal{O}(N^3+N^22^N+q\log n)\)。說句閒話:我想用離線查詢 + 單隊維護凸包搞,於是調了一個下午和晚上(還沒調出來)。有李超這麼nb玩意,凸包🐶都不用!!!

#include<bits/stdc++.h>
using namespace std;
#define int long long
constexpr int N = 5e6 + 5, S = 1 << 18, Inf = 1e18;
int n, m, q, tot, val[20], dis[20][20], sum[S], f[19][S], cnt[19], rt[19];
inline void init(){
	for(int i=1; i<=n; ++i){
		for(int j=1; j<=n; ++j) if(i ^ j) dis[i][j] = Inf;
		for(int j=0; j<tot; ++j) f[i][j] = Inf;
	}
}
namespace LCST{
	struct Seg{
		int k, b;
		int operator () (const int x) const{ return k * x + b; }
	}s[N];
	int cnt, tot, t[N], ls[N], rs[N];
	inline void modify(int& id, int l, int r, int w){
		if(!id) return void(t[id = ++cnt] = w);
		if(l == r) return void(s[w](l) > s[t[id]](l) ? t[id] = w : 0);
		int mid = (l + r) >> 1;
		if(s[w](mid) > s[t[id]](mid)) swap(w, t[id]);
		if(s[w](l) >= s[t[id]](l)) modify(ls[id], l, mid, w);
		if(s[w](r) >= s[t[id]](r)) modify(rs[id], mid+1, r, w);
	}
	inline int query(int id, int l, int r, int x){
		if(!id) return 0;
		int ans = s[t[id]](x), mid = (l + r) >> 1;
		if(x <= mid) ans = max(ans, query(ls[id], l, mid, x));
		else ans = max(ans, query(rs[id], mid+1, r, x));
		return ans;
	}
	inline void push_in(int &rt, int k, int b){
		s[++tot] = Seg{k, b};
		modify(rt, 1, 1e9, tot);
	}
}
signed main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>n>>m; tot = 1 << n; init();
	for(int i=1; i<=n; ++i) cin>>val[i];
	for(int i=1, u, v, w; i<=m; ++i)
		cin>>u>>v>>w, dis[u][v] = w;
	for(int k=1; k<=n; ++k) for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j)
		if((k ^ i) && (k ^ j) && (i ^ j))
			dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
	for(int s=1; s<tot; ++s){
		for(int i=1; i<=n; ++i) if(s & (1<<(i-1))){
			sum[s] += val[i];
			int tmp = s ^ (1 << (i-1));
			if(!tmp){ f[i][s] = 0; break; }
			for(int k=1; k<=n; ++k) if(tmp & (1<<(k-1))){
				if(dis[k][i] == Inf) continue;
				f[i][s] = min(f[i][s], f[k][tmp] + sum[tmp] * dis[k][i]);
			}
		}
	}
	for(int i=1; i<=n; ++i){
		for(int s=1; s<tot; ++s) if(s & (1<<(i-1))){
			if(f[i][s] >= Inf) continue;
			LCST::push_in(rt[i], sum[s], -f[i][s]);
		}
	} cin>>q; while(q--){
		int s, e; cin>>s>>e;
		cout<<LCST::query(rt[e], 1, 1e9, s)<<'\n';
	} return 0;
}

F [USACO23JAN] Subtree Activation P

這道題很好手模,很好發現的是:如果你想完成 \(u\)\(v\) 節點以及他們的公共祖先 \(w\),那麼存在最有策略是依次點亮 \(u\sim w\sim v\) 及其子樹,然後依次滅掉 \(u\sim w\sim v\) 的子樹,這樣同時完成了 \(u,w,v\) 的要求,可以得到的是,這樣的最優操作次數恰好為 \(2siz_w\)

當我還在苦逼找規律的時候,大神已經開始設計 DP 了。令 \(f_{u,0}\) 表示覆蓋 \(u\) 的子樹的最小操作次數。\(f_{u,1}\) 表示有且僅有一條從 \(u\)\(u\) 的子樹內某節點 \(v\) 的鏈沒有被點亮,而 \(u\) 子樹其他節點都被覆蓋的最小次數。這樣設計有個顯而易見的好處:合併鏈的代價變得可計算。

  • 對於 \(f_{u,1}\),可以看成在 \(u\) 的兒子節點裡找一個兒子並把它的鏈向上延長到 \(u\),相應地,其它兒子及其子樹都應該被覆蓋過了。於是有:

    \[\begin{split} f_{u,1}&=\min_{v\in son_u}(f_{v,1}+\sum_{v'\in son_u,v'\not=v}f_{v',0}) \\ &=\sum_{v\in son_u}f_{v, 0}+\min_{v'\in son_u}(f_{v',1}-f_{v',0}) \end{split} \]

  • 對於 \(f_{u,0}\),分為兩種情況:

    • 假設 \(u\) 的所有兒子及其子樹都已經被覆蓋過了,那麼就找個兒子並把它的鏈延長至 \(u\) 並加上額外貢獻。

      \[f_{u,0}=\sum_{v\in son_u}f_{v,0}+2(siz_u-\max_{v\in son_u}(siz_v)) \]

    • 假設 \(u\) 還有兩個兒子沒被覆蓋,那麼將這兩個兒子的鏈延長到 \(u\) 然後一併統計一下貢獻:

      \[\begin{split} f_{u,0}&=2siz_u+\min_{v_1\in son_u,v_2\in son_u,v1\not=v_2}(f_{v1,1}+f_{v2,1}+\sum_{v_3\in son_u,v_3\not=v_1,v_3\not=v_2}f_{v_3,0})\\ &=2siz_u+\sum_{v\in son_u}f_{v,0}+\min_{v_1\in son_u,v_2\in son_u,v1\not=v_2}(f_{v1,1}-f_{v1,0}+f_{v2,1}-f_{v2,0}) \end{split} \]

#include<bits/stdc++.h>
using namespace std;
#define int long long
constexpr int N = 2e5 + 5, Inf = 1e18;
int n, f[2][N], siz[N];
vector<int> G[N];
inline void dfs(int u){
	siz[u] = 1; if(!G[u].size()) return void(f[0][u] = 2);
	int mn1 = Inf, mn2 = Inf, sum = 0, mx = 0;
	for(int v : G[u]){
		dfs(v); siz[u] += siz[v];
		mx = max(mx, siz[v]);
		sum += f[0][v];
		int res = f[1][v] - f[0][v];
		if(mn1 > res) mn2 = mn1, mn1 = res;
		else if(mn2 > res) mn2 = res;
	}
	if(mn1 != Inf) f[1][u] = sum + mn1;
	f[0][u] = sum + 2*(siz[u] - mx);
	if(mn1 != Inf && mn2 != Inf)
		f[0][u] = min(f[0][u], 2*siz[u] + sum + mn1 + mn2);
}
signed main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>n; for(int i=2, fa; i<=n; ++i)
		cin>>fa, G[fa].push_back(i);
	dfs(1); return cout<<f[0][1], 0;
}

性質分析、題目轉換、構造 DP一條龍。

G [USACO23FEB] Hungry Cow P

一眼線段樹分治,但不知道如何實現把大的數改小的操作。動態開點維護每個節點的有乾草的節點數和貢獻不用說了,還要維護從左邊傳進來的乾草數,以及右兒子的貢獻。

因為修改每一天的乾草數可能增大可能減小,所以標記是不能永久打在節點上面的。這是最重要的一點。

感覺這種題就是逢山開路,遇水搭橋那種的。在寫的過程中,需要什麼就維護什麼。

#include<bits/stdc++.h>
using namespace std;
#define it __int128
#define int long long
constexpr int N = 5e6 + 5, M = 1e9 + 7, Inv2 = 5e8 + 4;
int q, rt;
namespace ST{
	int cnt;
	struct node{ int ls, rs, sum, num, rgt, pas; }t[N];
	inline int query(int id, int l, int r, int val){
		if(!val) return t[id].sum;
		if(l == r) return val ? l % M : t[id].sum;
		int mid = (l + r) >> 1, ls = t[id].ls, rs = t[id].rs;
		if(t[ls].num + val <= mid + 1 - l)
			return (query(ls, l, mid, val) + t[id].rgt) % M;
		int ans = (it)(l + mid) * (mid - l + 1) % M * Inv2 % M;
		ans = (ans + query(rs, mid+1, r, t[ls].num + val - (mid - l + 1) + t[ls].pas)) % M;
		return ans;
	}
	inline void pushup(int id, int l, int r){
		int ls = t[id].ls, rs = t[id].rs, mid = (l + r) >> 1;
		t[id].num = t[ls].num + min(t[rs].num + t[ls].pas, r - mid);
		t[id].pas = t[rs].pas + max(t[ls].pas + t[rs].num - (r - mid), 0ll);
		t[id].rgt = query(rs, mid+1, r, t[ls].pas);
		t[id].sum = (t[ls].sum + t[id].rgt) % M;
	}
	inline void modify(int &id, int l, int r, int pos, int val){
		if(!id) id = ++cnt;
		if(l == r){
			if(val) t[id].sum = l % M, t[id].num = 1, t[id].pas = val - 1;
			else t[id].sum = t[id].num = t[id].pas = 0;
			return;
		} int mid = (l + r) >> 1;
		if(pos <= mid) modify(t[id].ls, l, mid, pos, val);
		else modify(t[id].rs, mid+1, r, pos, val);
		pushup(id, l, r);
	}
} using namespace ST;
signed main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>q; while(q--){
		int x, y; cin>>x>>y;
		modify(rt, 1, 2e14, x, y);
		cout<<t[rt].sum<<'\n';
	} return 0;
}

雙倍經驗 樓房重建

H [USACO23FEB] Problem Setting

感覺題意很好轉化,就是先放一大堆 E 再放一大堆 H,當然,每一道題對於每一個驗題人的難度不一定都相同。當放置一道題 \(i\) 的時候,第 \(i+1\) 道題必須是認為 \(i\) 題難的所有人都認為難的題。再聯絡到 \(m\le 20\),我們可以找到一個點集 \(s\) 表示所有狀態為 \(1\) 的驗題人都覺得這道題難,那麼 \(s\) 的上一個集合一定是 \(s\) 的一個子集。考慮維護出所有的點集 \(s\) 以及其對應的所有題目,然後狀壓 DP 轉移,所有狀態的方案數之和即為答案。拼盡全力也只能想到列舉子集的暴力 DP……

Warning:列舉子集一定要等列舉到 \(0\) 之後再 break 掉!!!

暴力 \(\mathcal{O}(3^m)\) 60pts

#include<bits/stdc++.h>
using namespace std;
#define ll long long
constexpr int N = 1e5 + 5, S = 1 << 20, M = 1e9 + 7;
int n, m, num[S], tot, sum[N], ans, f[S];
string str[21];
int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>n>>m; tot = 1 << m; sum[1] = 1;
	for(int i=2; i<=n; ++i) sum[i] = ((ll)i + (ll)sum[i-1] * i % M) % M;
	for(int i=1; i<=m; ++i) cin>>str[i];
	for(int i=1; i<=n; ++i){
		int tmp = 0;
		for(int j=1; j<=m; ++j) if(str[j][i-1] == 'H')
			tmp |= 1 << (j-1);
		++num[tmp];
	}
	for(int s=0; s<tot; ++s) num[s] = sum[num[s]];
	ans = f[0] = num[0];
	for(int s=1; s<tot; ++s){
		f[s] = 1;
		for(int tmp=(s-1) & s; tmp>=0; tmp = (tmp-1) & s){
			f[s] = ((ll)f[s] + (ll)f[tmp]) % M;
			if(!tmp) break;
		}
		f[s] = (ll)f[s] * num[s] % M;
		ans = ((ll)ans + (ll)f[s]) % M;
	} return cout<<ans, 0;
}

官方正解 FMT,於是我就去學了一下。好的,這篇部落格正式變成 FMT 學習筆記!


FMT 快速莫比烏斯變換(Fast Möbius Transform)

要求:

\[\hat{f}(S)=\sum_{T\subseteq S}f(T) \]

不知道咋讀)定義:

\[\hat{f}(S,i)=\sum_{T\in S}f(T)[\{i+1,i+2,i+3,\dots ,n\}\subseteq S] \]

於是有 \(\hat{f}(S,0)=f(s)\)\(\hat{f}(S,|S|)=f(\varnothing)\)

\(S \cap \{i\}=\varnothing\),所以 \(\hat{f}(S,i)=\hat{f}(S,i-1)\)。因為 \(\hat{f}(S,i)\) 表示 \(\{i+1,i+2,\dots,n\}\) 一定包含於 \(T\),並且一定沒有選 \(i\)\(\hat{f}(S\cup \{i\},i-1)\) 表示 \(\{i+1,i+2,\dots,n\}\) 一定包含於 \(T\),並且一定選擇了 \(i\)。依據加法原理,那麼有:

\[\hat{f}(S\cup\{i\},i)=\hat{f}(S,i-1)+\hat{f}(S\cup \{i\},i-1) \]

可以用 \(\mathcal{O}(n2^n)\) 的複雜度遞推求解子集和。當然也可以求解單個 \(f(S)\)

\[f(S)=\sum_{\{i\}\subseteq S}^{n}\hat{f}(\complement_{T}\{i\},i) \]

WC,剛看一篇部落格說它可以理解為高維字首和狀物,一下子變得清晰了。我上面寫的是 (偏數學一點)。

對於求解一維字首和,我們有:

for(int i=1; i<=n; ++i) s[i] += s[i-1];

對於二維,一般來說用的最多的是容斥,不過仍然可以一維一維地處理:

for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j) s[i][j] += s[i-1][j];
for(int i=1; i<=n; ++i) for(int j=1; j<=n; ++j) s[i][j] += s[i][j-1];

對於更高的維度也是如此。對於每一維都為 \(0,1\) 的維度,有:

for(int i=1; i<2; ++i) for(int j=1; j<2; ++j) ... s[i][j]... += s[i^1][j]...
...

不妨使用狀壓完成此過程,就有:

for(int i=0; i<(1<<n); ++i) for(int j=0; j<n; ++j)
    if(i & (1<<j)) s[i] += s[i^(1<<j)];

這不就是求子集和嗎?FMT 正是利用此性質將複雜度大大的最佳化。


回到此題,那我們就可以在遞推 \(\hat{f}\) 的同時處理出 \(f\) 陣列,大大降低了列舉子集的複雜度。總複雜度 \(\mathcal{O}(m2^m)\)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
constexpr int N = 1e5 + 5, S = 1 << 20, M = 1e9 + 7;
int n, m, num[S], tot, sum[N], ans, f[S], sf[21][S];
string str[21];
int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>n>>m; tot = 1 << m; sum[1] = 1;
	for(int i=2; i<=n; ++i) sum[i] = ((ll)i + (ll)sum[i-1] * i % M) % M;
	for(int i=1; i<=m; ++i) cin>>str[i];
	for(int i=1; i<=n; ++i){
		int tmp = 0;
		for(int j=1; j<=m; ++j) if(str[j][i-1] == 'H')
			tmp |= 1 << (j-1);
		++num[tmp];
	}
	for(int s=0; s<tot; ++s) num[s] = sum[num[s]];
	for(int s=0; s<tot; ++s){
		f[s] = 1;
		for(int i=0; i<m; ++i) if(s & (1<<i))
			f[s] = ((ll)f[s] + (ll)sf[i][s^(1<<i)]) % M;
		f[s] = (ll)f[s] * num[s] % M;
		ans = ((ll)ans + (ll)f[s]) % M; sf[0][s] = f[s];
		for(int i=1; i<m; ++i){
			sf[i][s] = sf[i-1][s];
			if(s & (1<<(i-1))) sf[i][s] = ((ll)sf[i][s] + (ll)sf[i-1][s^(1<<(i-1))]) % M;
		}
	} return cout<<ans, 0;
}

I [USACO23FEB] Watching Cowflix P

一眼虛樹好吧,然後不會。看了題解的虛樹做法,對於我這種菜雞不可做,考慮根號分治。

首先有樹形 DP。令 \(f_{1/0,i}\) 表示 \(i\) 在或不在聯通塊內。那麼有:

\[\begin{split} f_{0,u}&=\sum_{v\in son_u}min(f_{0,v},f_{1, v}+k)\\ f_{1,u}&=\sum_{v\in son_u}min(f_{1,v},f_{0,v}) \end{split} \]

DP需要使用 DFS序最佳化。暴力巨好打。然後打表可知,答案是單調不降的,並且相鄰兩個答案之間的差值也是單調不降的。觀察到對於 \(\sqrt n\) 以內的答案差值變化較大,直接暴力處理。當 \(k>\sqrt n\) 之後答案差值變化較小且連續,直接二分找到每一個差值相等的區間求解即可。碼好打。這題還能這麼做!!!

#include<bits/stdc++.h>
using namespace std;
constexpr int N = 2e5 + 5, Inf = 1e9;
int n, f[2][N], rnk[N], tot, fa[N];
string str; vector<int> G[N];
inline void dfs(int u, int f){
	rnk[++tot] = u;
	for(int v : G[u]){
		if(v == f) continue;
		fa[v] = u; dfs(v, u);
	}
}
inline int solve(int k){
	for(int i=1; i<=n; ++i)
		f[1][i] = 1, f[0][i] = str[i-1] == '0' ? 0 : Inf;
	for(int i=n; i>1; --i){
		int v = rnk[i], u = fa[v];
		f[0][u] += min(f[0][v], f[1][v] + k);
		f[1][u] += min(f[1][v], f[0][v]);
	} return min(f[0][1], f[1][1] + k);
}
int main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>n>>str; int len = sqrt(n);
	for(int i=1, u, v; i<n; ++i){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	} dfs(1, 0);
	for(int i=1; i<=len; ++i) cout<<solve(i)<<'\n';
	int l = len + 1, r = n, ans = solve(len + 1), bg, del = ans - solve(len);
	do{
		bg = l;
		while(l < r){
			int mid = (l + r + 1) >> 1;
			if(solve(mid) - solve(mid - 1) < del) r = mid - 1;
			else l = mid;
		} 
		bg = l - bg + 1;
		do{
			cout<<ans<<'\n';
			--bg; ans += del;
		} while(bg);
		l = l + 1, r = n, ans = solve(l), del = ans - solve(l - 1);
	} while(l <= n);
	return 0;
}

以後這種題暴力打完先找找規律再去想正解。

J [USACO23OPEN] Pareidolia P

8 pts 的 \(\mathcal{O}(N^3)\) 暴力還是很好打的。若想最佳化到 \(\mathcal{O}(N^2)\) 查詢 \(\mathcal{O}(N)\) 需要用到 DP。令 \(f_{i,0\sim5}\) 表示所有以第 \(i\) 個字元為末尾的字串中匹配到了 bessie\(0\sim5\) 的位置的數量,類似於一個桶,很好寫出 20 pts暴力。

#include<bits/stdc++.h>
using namespace std;
#define int long long
constexpr int N = 2e5 + 5;
string str;
int n, q, f[N][6];
inline int solve(){
	for(int i=1; i<=n; ++i) for(int j=0; j<6; ++j) f[i][j] = 0;
	int ans = 0, res = 0;
	for(int i=1; i<=n; ++i){
		for(int j=0; j<6; ++j) f[i][j] = f[i-1][j];
		switch(str[i]){
			case 'b': f[i][1] += f[i][0], f[i][0] = 0; break;
			case 'e': f[i][2] += f[i][1], f[i][1] = 0;
					  res += f[i][5], f[i][0] += f[i][5], f[i][5] = 0; break;
			case 's': f[i][4] += f[i][3], f[i][3] = 0;
					  f[i][3] += f[i][2], f[i][2] = 0; break;
			case 'i': f[i][5] += f[i][4], f[i][4] = 0; break;
		}
		++f[i][str[i] == 'b' ? 1 : 0];
		ans += res;
	} return ans;
}
signed main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>str>>q; n = str.size(); str = ' ' + str;
	cout<<solve()<<'\n';
	while(q--){
		int x; char ch;
		cin>>x>>ch; str[x] = ch;
		cout<<solve()<<'\n';
	}
}

正解動態 DP。黑科技。就是線段樹 + 矩陣乘法。把 DP 的遞推過程轉化為矩陣乘法,線段樹每一個節點都是一個矩陣。唯一難點在於轉移矩陣的設計。剩下的就是個綠。注意卡空卡時。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define f(i) for(int i=0; i<9; ++i)
constexpr int N = 2e5 + 5;
string str; int n, q;
struct Matrix{ int m[9][9]; } bas;
Matrix operator * (const Matrix &a, const Matrix &b){
	Matrix c; memset(c.m, 0, sizeof(c.m));
	f(i) f(j) f(k) c.m[i][j] += a.m[i][k] * b.m[k][j];
	return c;
}
inline Matrix get(char ch){
	Matrix a; memset(a.m, 0, sizeof(a.m));
	f(i) a.m[i][i] = 1; a.m[1][0] = 1;
	switch(ch){
		case 'i': a.m[6][6] = 0, a.m[6][7] = 1; break;
		case 's': a.m[4][4] = a.m[5][5] = 0, a.m[4][5] = a.m[5][6] = 1; break;
		case 'b': a.m[2][2] = 0, a.m[2][3] = 1; break;
		case 'e': a.m[3][3] = a.m[7][7] = 0; a.m[7][0] = a.m[7][1] = a.m[7][2] = a.m[3][4] = 1; break;
	} a.m[8][ch == 'b' ? 3 : 2] = 1;
	return a;
}
namespace ST{
	#define ls (id << 1)
	#define rs (id << 1 | 1)
	Matrix t[N<<2];
	inline void pushup(int id){
		t[id] = t[ls] * t[rs];
	}
	inline void build(int id, int l, int r){
		if(l == r) return void(t[id] = get(str[l]));
		int mid = (l + r) >> 1;
		build(ls, l, mid), build(rs, mid+1, r);
		pushup(id);
	}
	inline void modify(int id, int l, int r, int pos){
		if(l == r) return void(t[id] = get(str[l]));
		int mid = (l + r) >> 1;
		if(pos <= mid) modify(ls, l, mid, pos);
		else modify(rs, mid+1, r, pos);
		pushup(id);
	}
}
inline int getans(){
	Matrix ans = bas * ST::t[1];
	return ans.m[0][0];
}
signed main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>str>>q; n = str.size(); str = ' ' + str;
	bas.m[0][8] = 1;
	ST::build(1, 1, n); cout<<getans()<<'\n';
	while(q--){
		int x; char ch; cin>>x>>ch;
		str[x] = ch; ST::modify(1, 1, n, x);
		cout<<getans()<<'\n';
	} return 0;
}

K [USACO23OPEN] Good Bitstrings P

觀察了一會兒性質,發現 \(S\)\((a+b)/\gcd(a,b)\) 一個迴圈,感覺和 \(\gcd\) 有關,找了半天規律也沒找出個啥。學習一下題解。

考慮將所有時刻的 \(ia,ib\) 作為座標畫到平面直角座標系上,然後分析性質。

  • 那麼有射線 \((a,b)\),如果某一點在 \((a,b)\) 下方,那麼下一步一定是往上走的,如果在上方,那麼下一步一定是往右走的。

    可以對題目程式碼的判斷式進行移項:\(\frac{ia}{ib}\le\frac{a}{b}\) 往右走,反之往左走。

  • \((x,y)\) 合法當且僅當所有 \(0\le x'<x,0\le y' < y\) 的點都在射線 \((a,b)\)\((x,y)\) 同側。

    反證法證明:假設 \((a,b)\times (x,y)>0,k_{a,b}>k_{x,y}\),點 \((x',y')\) 在異側,那麼對於射線 \((a,b)\),這個點要往上走,而對於射線 \((x,y)\),這個點要往右走,矛盾。

  • 對於滿足 \(f_1\times f_2=1\) 的整點 \(f_1,f_2\),任意滿足 \(f_1\times f > 0\) 並且 \(f\times f_2>0\) 的整點 \(f\) 都有 \(f=k_1f_1+k_2f_2\)\(k_1,k_2\) 皆為正整數。

    怎麼理解呢?就是任意整點(向量) \(f\) 都可以被分解為兩個滿足叉乘為 \(1\) 的整點(向量)之和。

    必要性證明:構造 \(k_2=f_1\times f, k_1=f\times f_2\),於是有 \(f_1\times f=f_1 \times (k_1f_1+k_2f_2),f\times f_2=(k_1f_1+k_2f_2)\times f_2\),從而 \(f=(f\times f_2)f_1+(f_1\times f)f_2\)。由於 \(f_1,f_2,f\) 均為整點,所以 \(k_1,k_2\) 均為正整數。又因為向量分解為兩個非平行向量的分解方式唯一,所以必要性得證。

    有了這一條性質,就可以考慮遞推求所有合法點:

    1. 初始令 \(f_1=(0,1),f_2=(1,0)\),滿足 \(f_1\times f_2=1\),過程保證射線 \(f_1,f_2\) 之間的點都被統計過,並且 \(f_1,f_2\) 均為合法點。
    2. \(f=f_1+f_2\),則 \(f\) 為合法點。接下來按照 \(f\) 對於 \((a,b)\) 的位置決定令 \(f_1=f\)\(f_2=f\) 繼續向下遞迴。
    3. \(f_2\times f=0\),結束演算法。

    但是這個演算法漏統計了若干個形如 \(kf_2\) 的點,考慮完善一下。

  • 若當前的 \(f\) 滿足 \(f\times (a,b)>0\),記在此之前 \(f_1\) 已經連續變化了 \(k\) 次,則 \((k+2)f_2\) 合法。

    到這裡已經看不懂了……😵

  • 演算法結束時一定有 \(f_2=(\frac{a}{\gcd(a,b)},\frac{b}{\gcd(a,b)})\),且 \(f_1\times (a,b)=\gcd(a,b)\)

#include<bits/stdc++.h>
using namespace std;
#define int long long
int T, a, b;
inline int solve(int a, int b){
	int f1 = b, f2 = a, ans = 0, co = 1;
	while(f2){
		if(f1 > f2) ans += co * ((f1 - 1) / f2), f1 = (f1 - 1) % f2 + 1;
		else ans += f2 / f1, f2 %= f1, co = 2;
	} return ans + 2 * (f1 - 1);
}
signed main(){
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	cin>>T; while(T--)
		cin>>a>>b, cout<<solve(a, b)<<'\n';
	return 0;
}

大坑待補。

L [USACO23OPEN] Triples of Cows P

黑的。

相關文章