2024.10.21 雜題

PassName發表於2024-10-21

2024.10.21 雜題

P11217 【MX-S4-T1】「yyOI R2」youyou 的垃圾桶

\(O(n \log n)\) 線段樹二分不會,想寫 \(O(q \log ^ 2n)\) 的二分,但是 htdlz 說常數大可能過不去。所以我選擇寫樹狀陣列實現的 \(O(q \log^2 n)\) 做法然後跑的飛快比線段樹二分還快直接過了(doge)

記錄字首和 \(s[i]\),由於我們寫的樹狀陣列沒有建樹操作。所以對於每一次 \(add(l,r,k)\) 我們的實際 \(\sum a_i\)\(s[n]+ask(n)\)。用 \(cnt\) 記錄我們能被桶踹多少輪,由於每一波傷害翻倍。所以 \(cnt\)\(\log_{2}{\frac{w}{s[n]+ask(n)}}\)。再記錄被踹到最後一輪剩了多少血。如果剛好被踹死,答案就是 \(cnt \times n - 1\)。如果還有一點血,就二分計算在下標為多少的時候被踹死。二分是 \(\log n\) 的,但是每次二分都要呼叫 \(ask\)\(ask\) 也是 \(\log n\) 的,所以總複雜度 \(O(n \log ^ 2n)\)

#include <bits/stdc++.h>

#define rint register int
#define int long long
#define endl '\n'

using namespace std;

const int N = 2e5 + 5;

int n, q, w;
int a[N];
int s[N];

struct BIT
{
    int c[2][N];
	
	int lowbit(int x){return x & -x;}
	
	void _add(int k, int x, int y) 
	{
		for (; x <= n; x += lowbit(x)) 
		    c[k][x] += y;
	}
	
	int _ask(int k, int x) 
	{
		int res = 0;
		for (; x; x -= lowbit(x)) 
		    res += c[k][x];
		return res;
	}
	
	void add(int l, int r, int k)
	{
		_add(0, l, k), _add(0, r + 1, -k);
		_add(1, l, l * k), _add(1, r + 1, -(r + 1) * k);	
	}
	
	int ask(int x)
	{
		return (x + 1) * _ask(0, x) - _ask(1, x);
	}	
} tree;

int calc(int x, int t) 
{
	int l = 1, r = n;
	while (l < r) 
	{
		int mid = (l + r) >> 1;
		if ((s[mid] + tree.ask(mid)) * t >= x) r = mid;
		else l = mid + 1;
	}
	return l;
}

signed main() 
{
	ios::sync_with_stdio(0); 
	cin.tie(0), cout.tie(0);
	cin >> n >> q >> w;
	for (rint i = 1; i <= n; i++) 
	{
		cin >> a[i];
		s[i] = s[i - 1] + a[i];
	}
	while (q--) 
	{
		int l, r, k;
		cin >> l >> r >> k;
        tree.add(l, r, k);
		int p = s[n] + tree.ask(n);
		// 因為原來的答案並沒有加入到樹狀陣列裡 所以為 s[n]+ask(n)
		int cnt = 0;//記錄能打多少輪
		cnt = log2(w / p + 1);
		int ck = w - ((1ll << cnt) - 1) * p;//看看打完後剩多少血
		if (!ck) //剛好死掉
		{
			cout << cnt * n - 1 << endl;
			continue;
		}
		int ans1 = calc(ck, 1ll << cnt) - 1;//二分答案計算最後一次在哪個位置死掉
		cout << cnt * n + ans1 << endl;
	}
	return 0;
}

P11218 【MX-S4-T2】「yyOI R2」youyou 不喜歡夏天

觀察資料範圍,複雜度瓶頸不能超過 \(n \log n\)。想帶個 \(\log\) 又不能排序又不能資料結構維護。那正解應該是 \(O(n)\) 的。考慮貪心,只能觀察出一些性質。

以一列為單位,先不考慮是不是聯通塊,對於兩黑或兩白是簡單的,兩黑一起選了一定更優,兩白一定儘可能不選。對於一黑一白有兩種玩法,第一種是如果 \(m\) 特別小 \(n\) 特別大,那麼儘可能多的選一黑一白中的黑,不選白。因為多選一個黑就能賺一點,\(m\) 很小 yy 不能怎麼改變結局。或者一黑一白一起選了 yy 就算翻轉了對答案也沒影響。

問題在於,什麼時候選擇一黑一白策略?什麼時候選擇一黑策略?怎麼判斷是不是聯通塊?貪心顯然是不行了。由於是一列一列掃的,每一列都有很強的關聯性。並且對於聯通塊左邊界 \(i\),從 \(i\) 開始掃,如果答案不優秀了要把聯通塊斷開重新算,dp 可以維護這個。所以考慮進行 dp。

\(f_i\) 表示在選擇一黑一白的時候兩個一塊辦了的答案,\(g_i\) 表示在選擇一黑一白的時候只選擇一個黑的答案。陣列 w[i] = (c[i] - '0') * 2 + (d[i] - '0') 以此來記錄當前列的形式。

如果 \(w_i=3\),那麼 \(f_i=f_{i-1}+2\)\(g\) 的轉移一樣。

如果 \(w_i = 0\),那麼只選一個白的作為過渡或者直接斷開重新開始,\(f_i = max\{f[i - 1] - 1, 0\}\)\(g[i]\) 同理

如果 \(w_i=1/2\),那麼 \(f_i=f_{i-1}\),因為選上這一列對答案沒影響。而對於 \(g\) 考慮的就多了,特判如果出現了兩個白隔斷不能進行只選一個的操作 ,以及兩種不同的一黑一白交替出現了導致只選黑色不能連起來,沒有這些這些情況 \(g_i = g_{i - 1} + 1\),否則 \(g_i=g_{i-1}\)。開個 \(lst\) 輔助判斷情況。

對於 \(f_i\) ,最後的答案就是 \(f_i\),而對於 \(g_i\),由於有很多一黑一白只選一個黑的情況,yy 可以透過翻轉操作讓他變成白從而讓答案減小 \(2\),所以 \(g_i\) 最後的答案要減去一個 \(2\times m\)

對於最終的答案就是 \(\max(\max_{i=1}^{n}(g_i-2\times m),\max_{i=1}^{n}f_i)\)

誒,問題來了,我們的 \(g\) 轉移其實是有問題的,因為我們可能中間選擇的一黑一白選一黑的情況並不夠 \(m\) 次導致最後計算出來的答案偏小。沒關係噠,因為我們最終的答案是對多個答案取最大值,而我們的考慮不周只是會導致部分 \(g_i\) 算的比較小,但是不會影響最終結果。

複雜度 \(O(n)\)

#include <bits/stdc++.h>

#define rint register int
#define int long long
#define endl '\n'
#define m(a) memset(a, 0, sizeof a)

using namespace std;

const int N = 2e6 + 5;
const int inf = 1e9;

int C, T, n, m;
int a[N], b[N];
int w[N];
int f[N], g[N];
/*
f[i] 表示在選擇一黑一白的時候兩個一塊辦了的答案
g[i] 表示在選擇一黑一白的時候只選擇一個黑的答案
*/
char c[N], d[N];

signed main() 
{
	cin >> C >> T;
	while (T--) 
	{
		cin >> n >> m;
		int lst = 0, ans = 0;
		scanf("%s%s", c + 1, d + 1);
		for (rint i = 1; i <= n; i++) w[i] = (c[i] - '0') * 2 + (d[i] - '0');
		m(g), m(f);
		for (rint i = 1; i <= n; i++) 
		{
			if (w[i] == 3)
			{
				f[i] = f[i - 1] + 2;
				g[i] = g[i - 1] + 2;
				lst = 3;				
			}
			else if (w[i] == 0) 
			{
				f[i] = max(f[i - 1] - 1, 0ll);
				if (g[i - 1] > 1) g[i] = g[i - 1] - 1;
				else g[i] = 0, lst = 3;
			} 
			else 
			{
				f[i] = f[i - 1];
				//這個時候為一黑一白 f[]兩個一起選了 答案不變
				if ((lst + w[i]) != 3)
				/*
				特判如果出現了兩個白隔斷不能進行只選一個的操作 
				以及兩種不同的一黑一白交替出現了導致只選黑色不能連起來
				沒有這些這些情況 g[i] = g[i - 1] + 1
				*/
				{
					g[i] = g[i - 1] + 1;
					lst = w[i];					
				}
				else 
				{
					g[i] = g[i - 1];
					lst = 3;
				}
			}
			ans = max({ans, g[i] - 2 * m, f[i]});
		}
		cout << ans << endl;
	}
	return 0;
}

P11219 【MX-S4-T3】「yyOI R2」youyou 的序列 II

感謝 ReTF 提供的幫助

首先,如果詢問的區間中含有大於 \(w_1\) 的數字,那麼 youyou 必然失敗。

稱長度為 \(c_2\),總和大小大於 \(w_2\) 的子區間為合法區間,也即 yy 可以操作的區間。

  • 性質 1:對於不存在任何一個“合法區間”的序列,youyou 顯然必勝
  • 性質 2:存在“合法區間”,若 youyou 可以一次性染紅所有未染紅的合法區間,則 youyou 必勝
  • 性質 3:存在“合法區間”,若 youyou 不可以做到一次性染紅所有未染紅的合法區間,則 yy 必勝

對於 yy 而言的最優策略是:儘量在整個序列的邊緣進行染色。yy 的目的是防止 youyou 把整個序列染成紅色。設所有“合法區間”中位於最左邊的左端點為 \(l\),最右邊的右端點為 \(r\)。如果滿足性質 3,只考慮位置 \(l,r\) ,若 youyou 每次把哪個點染了,yy 就可以跟著染,然後陷入迴圈,youyou 必敗。所以只要 youyou 無法一次染紅 \(l\)\(r\) ,則 yy 必勝。

直接維護原序列使用線段樹即可,非常簡單。實現瓶頸在於如何求出上述的 \(l,r\)

粉兔講的是線段樹二分,每個葉子記錄一個長度為 \(c_2\) 的區間,將單點修改轉為區間修改。

這裡採用的實現方式是使用勢能線段樹,每個點維護的是 [i,i + d - 1] 的元素和。那麼噹噹前點勢能為 \(0\) 時並且 \(l=r\),那麼將 [i,i + d - 1] 存入即可。儲存可以開個 set。由於每次 change 只會在 setinsert 一次,所以複雜度為 \(O(n \log n)\)

補充:關於勢能線段樹

之前並沒有整理過相關筆記。馬上退賽了也必要專門拿出來時間做筆記,大概記錄一下原理。很多區間修改操作是不能依靠懶標記完成,因為很多運算都是依賴於葉子節點的值的一直遞迴到葉子結點一個一個改顯然無法接受。每一個操作,總會使得其能夠接受的繼續進行修改的次數越來越少。比如一開始位於高空,每次修改使高度下降勢能變小,當勢能為 \(0\) 時再去對接來下的操作就沒有意義了可以直接停了。

#include <bits/stdc++.h>

#define rint register int
#define int long long
#define endl '\n'

#define ls p << 1
#define rs p << 1 | 1

using namespace std;

const int N = 3e5 + 5;
const int M = 1 << 20;
const int inf = 1e18;

int n, q, c1, c2, w1, w2;
int a[N];
set<int> L, R;
int s[N];

struct SegmentTree1
{
	struct node
	{
		int v, sum;
	} t[M]; 
	
	void push_up(int p)
	{
		t[p].v = max(t[ls].v, t[rs].v);
		t[p].sum = t[ls].sum + t[rs].sum;
	}
	
	void build(int p, int l, int r) 
	{
		if (l == r) 
		{
			t[p].v = t[p].sum = a[l];
			return;
		}
		int mid = (l + r) >> 1;
		build(ls, l, mid);
		build(rs, mid + 1, r);
		push_up(p);
	}

	void change(int p, int l, int r, int x, int d) 
	{
		if (l == r) 
		{
			t[p].v = t[p].sum = d;
			return;
		}
		int mid = (l + r) >> 1;
		if (x <= mid) change(ls, l, mid, x, d);
		else change(rs, mid + 1, r, x, d);
		push_up(p);
	}

	int query_sum(int p, int l, int r, int x, int y) 
	{
		if (x <= l && r <= y) return t[p].sum;
		int mid = (l + r) >> 1;
		int res = 0;
		if (x <= mid) res += query_sum(ls, l, mid, x, y);
		if (y > mid) res += query_sum(rs, mid + 1, r, x, y);
		return res;
	}

	int query_max(int p, int l, int r, int x, int y) 
	{
		if (x <= l && r <= y) return t[p].v;
		int mid = (l + r) >> 1;
		if (x <= mid) return query_max(ls, l, mid, x, y);
		if (y > mid) return query_max(rs, mid + 1, r, x, y);
		return max(query_max(ls, l, mid, x, y), query_max(rs, mid + 1, r, x, y));
	}
} tree1;

struct SegmentTree2
{
	struct node
	{
		int minn, lazy;
	} t[M]; 
	
	void push_up(int p)
	{
		t[p].minn = min(t[ls].minn, t[rs].minn);
	}

	void push_down(int p) 
	{
		t[ls].minn -= t[p].lazy, t[rs].minn -= t[p].lazy;
		t[ls].lazy += t[p].lazy, t[rs].lazy += t[p].lazy;
		t[p].lazy = 0;
	}

	void build(int p, int l, int r) 
	{
		t[p].minn = inf;
		if (l == r) return ;
		int mid = (l + r) >> 1;
		build(ls, l, mid);
		build(rs, mid + 1, r);
		push_up(p);
	}

	void pos_change(int p, int l, int r, int x, int y) 
	{
		if (l == r) 
		{
			t[p].minn = min(t[p].minn, y);
			return;
		}
		int mid = (l + r) >> 1;
		if (x <= mid) pos_change(ls, l, mid, x, y);
		else pos_change(rs, mid + 1, r, x, y);
		push_up(p);
	}

	void range_change(int p, int l, int r, int x, int y, int d) 
	{
		if (t[p].minn >= inf) return;
		if (x <= l && r <= y) 
		{
			if (t[p].minn > d)
			{
				t[p].minn -= d; 
				t[p].lazy += d;
			} 
			else if (l == r) 
			{
				t[p].minn = inf;
				L.insert(l);
				R.insert(l + c2 - 1);
			} 
			else 
			{
				push_down(p);
				int mid = (l + r) >> 1;
				range_change(ls, l, mid, x, y, d);
				range_change(rs, mid + 1, r, x, y, d);
				push_up(p);
			}
			return ;
		}
		push_down(p);
		int mid = (l + r) >> 1;
		if (x <= mid) range_change(ls, l, mid, x, y, d);
		if (y > mid) range_change(rs, mid + 1, r, x, y, d);
		push_up(p);
	}
} tree2;

int findL(int x) {return *L.lower_bound(x);}
int findR(int x) {return *--R.upper_bound(x);}

signed main() 
{
	cin >> n >> q >> c1 >> c2 >> w1 >> w2; 
	for (rint i = 1; i <= n; i++)
	{
		cin >> a[i];
		s[i] += s[i - 1] + a[i];
	} 
	if (c2 > n) c2 = n;
	tree1.build(1, 1, n);
	tree2.build(1, 1, n - c2 + 1);
	L.insert(0), L.insert(n + 1);
	R.insert(0), R.insert(n + 1);
	for (rint i = 1; i <= n - c2 + 1; i++) //列舉左端點
	{
		int sum = s[i + c2 - 1] - s[i - 1];//區間和
		if (sum <= w2) tree2.pos_change(1, 1, n - c2 + 1, i, w2 - sum + 1);
		//tree2 是一顆勢能線段樹
		//每個點維護的是 [i,i + d - 1] 的元素和
		else L.insert(i), R.insert(i + c2 - 1);//大於 w2 全選更優
	}
	while (q--) 
	{
		int opt;
		cin >> opt;
		if (opt == 1) 
		{
			int x, y;
			cin >> x >> y;
			a[x] += y;
			tree1.change(1, 1, n, x, a[x]);
			if (max(x - c2 + 1, 1ll) <= min(x, n - c2 + 1)) 
				tree2.range_change(1, 1, n - c2 + 1, max(x - c2 + 1, 1ll), min(x, n - c2 + 1), y);
			//單點修改同時在tree2更新儲存 l, r
		} 
		else 
		{
			/*
			稱長度為 c2,總和大小大於 w2 的子區間為合法區間 也即 yy 可以操作的區間
			*/
			int l, r;
			cin >> l >> r;
			if (tree1.query_max(1, 1, n, l, r) > w1) puts("tetris");
			// youyou 沒法出手直接輸掉
			else 
			{
				if (r - l + 1 <= c2) 
				{
					if (tree1.query_sum(1, 1, n, l, r) > w2) 
					// 如果 yy 能一次性全染上
					{
						if (r - l + 1 <= c1 && tree1.query_sum(1, 1, n, l, r) <= w1) puts("cont");
						// 如果 youyou 能一次全染上 則 youyou 一定能贏
						else puts("tetris");
						// 否則 yy 一直一次性全染上 youyou 贏不了
					} 
					else puts("cont");
					// yy 不能一次性全部解決掉,則 youyou 一定贏
				} 
				else 
				{
					int L = -1, R = -1;
					L = findL(l), R = findR(r);
					// 查詢當前情況左右端點
					if (L == -1 || R == -1 || L > R || R - L + 1 < c2) puts("cont");
					// 如果 L,R 沒找到或者 L 比 R 大又或者區間長度小於 c2
					// 即不存在合法區間
					// 那麼 youyou 一定贏
					else if (R - L + 1 <= c1 && tree1.query_sum(1, 1, n, L, R) <= w1) puts("cont");
					// 同樣,如果 youyou 能一次全染上 則 youyou 一定能贏
					else puts("tetris");
					// 否則 yy 贏
				}
			}
		}
	}
	return 0;
}