2024.10.16 模擬賽

PassName發表於2024-10-16

2024.10.16 模擬賽

T1 divide

簡要題意

給定一棵樹的 \(n\) 個結點以及每個結點的 \(fa_i\),每個點的點權 \(v_i\),刪除樹中的兩條邊,將樹拆分為三個非空部分。每個部分的權值等於該部分包含的所有節點的權值之和。出一種合理的拆分方案。根節點的 \(fa_i = 0\)

\(n≤10^6\)

solution

首先可以算出所有的點的點權和,如果不是 \(3\) 的倍數,那麼一定不存在合法的方案。

\(g=\sum v_i/3\)。也就是說,我們只需要求出每個樹的子樹大小就行了。其實這個過程直接迴圈掃一遍就行。但是,由於要拆分,直接迴圈掃無法實現拆下去使子樹大小減小的過程(實測得分為 45pts),所以直接 dfs,\(sum[i]\) 記錄子樹點權和。如果 \(sum[i]=g\) 就記錄答案,並且切去當前子樹令 \(sum[i]=0\)。最後合法位置超過三個則存在方案。否則輸出 \(-1\)

#include <bits/stdc++.h>

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

using namespace std;

const int N = 2e6 + 5;
const int M = N << 1;

int n;
int fa[N], v[N];
int h[N], e[M], ne[M], idx;
int sum[N];
int ans[N];
int top;
int goal;

void add(int a, int b)
{
	e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}

void dfs(int x)
{
	for (rint i = h[x]; i; i = ne[i])
	{
		int y = e[i];
		dfs(y);
		sum[x] += sum[y];
	}
	if (goal == sum[x]) 
	{
		ans[++top] = x;
		sum[x] = 0;
	}
}

signed main()
{
	cin >> n;
	int res = 0, root = 0;
	for (rint i = 1; i <= n; i++) 
    {
		cin >> fa[i] >> v[i];
		res += v[i];
		sum[i] = v[i];
		if (fa[i] == 0) root = i;
		else add(fa[i], i);
	}
	if (res % 3)
	{
		cout << -1 << endl;
		return 0;
	}
	goal = res / 3;
	dfs(root);
	if (top > 2) cout << ans[1] << " " << ans[2];
	else cout << -1 << endl;
    return 0;
}

T2 color

題目大意

給定一個 \(n\) 個結點的樹,每個節點都有顏色為黑或白,從黑色節點無法到達白色節點,反之亦然。每次染色操作可以選擇一個節點 \(v\),並改變節點 \(v\) 以及其所有可達同色節點的顏色。希望將樹中的所有節點都染成同一種顏色,求最少操作次數。

\(n≤2 \times 10^5\)

solution

神仙題

放個樣例,第一眼看過去,我的想法是維護類聯通塊的東西。如 \(2,1,3,8,9\) 為一個聯通塊。最後數黑色和白色聯通塊個數取 \(min\)

結果大樣例過不去。

很簡單就能 hack 掉,給一個一直黑白交錯的非常長單鏈,隨便接一個特別短的黑白交錯的支鏈,上面那種做法就不行了。因為答案會比正解小。

那怎麼做呢?

從特殊到一般,假設這是一條鏈。那麼答案就是設黑白交替的次數為 \(cnt\),答案為 \(⌊\frac{cnt}{2}⌋\)。那麼對於一棵樹,其實就是在這條鏈上加一些分支。

那麼,如果我這條鏈,為黑白交替次數最多的一條鏈,對於這棵樹的答案就等於對於這條鏈的答案。因為操作這條鏈的時候,其他鏈條的染色也一定會隨著這條鏈的染色而顏色統一。原因為支鏈的黑白交錯次數少並且染色操作為聯通著就能染色。

所以,只需要類比求樹的直徑來求這條黑白交錯次數最多的鏈條即可。這裡使用兩次 dfs,程式碼容易實現。先求出對於 \(x\) 黑白交錯次數最多的鏈條遠處端點為 \(y\),再求出對於 \(y\) 的最遠端點 \(z\),那麼 \(y->z\) 即為所求,然後求出 \(cnt\),即可求出答案。

複雜度 \(O(n)\)

#include <bits/stdc++.h>

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

using namespace std;

const int N = 2e5 + 5;
const int M = 4e5 + 5;

int n;
int a[N];
int h[N], e[M], ne[M], idx;
int dist, ed;

void add(int a, int b) 
{
	e[++idx] = b, ne[idx] = h[a], h[a] = idx;
}

void dfs(int x, int father, int w)
{
	if (w > dist)
	{
		dist = w;
		ed = x;
	}
	for (rint i = h[x]; i; i = ne[i])
	{
		int y = e[i];
		if (y == father) continue;
		dfs(y, x, w + (a[x] != a[y]));
	}
}

signed main() 
{
	cin >> n;
	for (rint i = 1; i <= n; i++) cin >> a[i];
	for (rint i = 1; i < n; i++)
    {
		int a, b;
		cin >> a >> b;
		add(a, b);
		add(b, a);
	}
	dfs(1, 0, 0);
	dist = 0;
	dfs(ed, 0, 0);
	cout << (dist + 1) / 2 << endl;
	return 0;
}

T3 count

簡要題意

對於一個長度為 \(n\) 的小數(包括小數點)執行最多 \(k\) 次四捨五入,四捨五入最多執行到小數點處,不於整數位進行四捨五入。求最後答案的最大值。

\(n≤2 \times 10^5,k≤10^9\)

solution

這個題卡了很久,因為卡在執行四捨五入操作上了。因為我在擔心當前四捨五入是否優秀,後來才發現,我所擔心的,類似於把 \(3.98\) 四捨五入成 \(4.08\).........(\(8\) 先不動跳過去,然後四捨五入 \(3.9\)

\(k\) 根本不用管它,複雜度瓶頸與它無關,它的上限不會影響操作的執行,但是下限會。

執行過程為,找到第一個大於等於 \(5\) 的位置執行四捨五入,此時一定是最優的,從此處開始四捨五入即可。每四捨五入一次 \(k--\),如果最後結束操作時如果 \(k<1\) 就說明整數位也進行了一次四捨五入,輸出的時候開頭加個 \(1\) 即可。

#include <bits/stdc++.h>

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

using namespace std;

int n, m;
string s;

signed main()
{
    cin >> n >> m; cin >> s;
    int pos = s.find('.');
    int k = pos + 1;
    while (k < s.size() && s[k] < '5') k++;
    if (k == s.size()) 
	{
		cout << s << endl;
		return 0;
	}
    s = s.substr(0, k); //後邊一定會被四捨五入成 0
    k--, m--; //進了一次位
    while (k >= 0)
    {
        if (s[k] != '.')
        {
        	s[k]++;
            if (s[k] < '5') break; //進位進不動了
            if (s[k] <= '9')
            {
                if (!m || k < pos) break;
                s[k] = '0';
                m--;
            }
            else s[k] = '0';
        }
        k--;
    }
    if (k < 0) s = "1" + s;
    while (s.back() == '0') s.pop_back();
    if (s.back() == '.') s.pop_back();
    cout << s << endl;
    return 0;
}

T4 change

簡要題意

給定一個長度為 \(n\) 的非負整數序列 \(a_1,a_2,…,a_n\)。其中的所有元素將被逐個封印。具體封印順序可以用一個 \(1\)\(n\) 的排 \(b_1,b_2,…,b_n\) 來描述,第 \(i\) 個被封印的元素即為 \(a_{b_i}\)

完成 \(n\) 個任務,第 \(i\) 個任務是:對於完成前 \(i\) 次封印的序列,請你找到序列中的一個連續子序列(可以為空),使得該子序列不含任何被封印的元素,且子序列內各元素之和儘可能大,輸出這個子序列元素和的最大可能值。空序列元素和為 \(0\)

\(n≤10^5\)

solution

正解是並查集進行貪心

但是我們可以進行一個投機取巧

這個題其實就是在求最大子序列和,但是中間有些位置不能選。那麼我們將不能選的位置設定成無窮小就行了。這樣就就算選了這個位置,也不會改變最終答案,因為選了它也不是最大的,這樣就可以正常使用線段樹進行維護了。

剩下的就是板子了

#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 = 1e5 + 5;
const int M = 2e6 + 5;

int n, m;
int w[N];
struct node 
{
	int l, r;
	int tmax, lmax, rmax, sum;
} t[M];

void push_up(node &p, node &l, node &r) 
{
	p.sum = l.sum + r.sum;
	p.lmax = max(l.lmax, l.sum + r.lmax);
	p.rmax = max(r.rmax, r.sum + l.rmax);
	p.tmax = max({l.rmax + r.lmax, l.tmax, r.tmax});
}

void push_up(int p) 
{
	push_up(t[p], t[ls], t[rs]);
}

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

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

node query(int p, int l, int r) 
{
	if (t[p].l >= l && t[p].r <= r) return t[p];
	int mid = (t[p].l + t[p].r) >> 1;
	if (r <= mid) return query(ls, l, r);
	else if (l > mid) return query(rs, l, r);
	else 
	{
		node left = query(ls, l, r);
		node right = query(rs, l, r);
		node res;
		push_up(res, left, right);
		return res;
	}
}

signed main() 
{
	cin >> n;
	for (rint i = 1; i <= n; i++) cin >> w[i];
	build(1, 1, n);
	m = n;
	while (m--) 
	{
		int y;
		cin >> y;
		change(1, y, -1e14);
		cout << max(0ll, query(1, 1, n).tmax) << endl;
	}
	return 0;
}