[AIO 2023] 题解

A

题目传送门

算法:纯模拟统计即可

更多操作?

有些人可能会在做题时想回程的时候用不用更多统计,但其实由于每次只能移动一步,所以在回程的时候相当于你已经走过了从原点到目前位置之间的所有地方,直接回程即可。

代码

#include <bits/stdc++.h>
using namespace std;
 
int n;
char c;
bool flag[200005];
int cnt;
 
int main()
{
	freopen("telein.txt", "r", stdin);
	freopen("teleout.txt", "w", stdout);
	
	cin >> n;
	int now = 200000 / 2;
	flag[now] = 1;
	cnt = 1;
	while (n--)
	{
		cin >> c;
		if (c == 'R')
		{
			now++;
			if (!flag[now])
			{
				flag[now] = 1;
				cnt++;
			}
		}
		if (c == 'L')
		{
			now--;
			if (!flag[now])
			{
				flag[now] = 1;
				cnt++;
			}
		}
		if (c == 'T')
			now = 200000 / 2;
	}
	cout << cnt << "\n";
	return 0;
}

提交记录

B

题目传送门

算法:模拟,桶数组

纯暴力

两层循环,枚举数字和数组个数。

显然,\(O(NK)\),过不了。

正解

使用桶数组在输入的时候统计一下即可。

代码

#include <bits/stdc++.h>
using namespace std;
 
int n, k;
int a[100005];
int cnt[100005];
bool flag[100005];
int ans = 100005;
 
int main()
{
	freopen("rafflein.txt", "r", stdin);
	freopen("raffleout.txt", "w", stdout);
	
	cin >> n >> k;
	for (int i = 1; i <= n; i++)
	{
		cin >> a[i];
		cnt[a[i]]++;
		if (cnt[a[i]] >= 2)
			flag[a[i]] = 1;
	}
	for (int i = 1; i <= n; i++)
	{
		if (flag[a[i]])
			continue;
		ans = min(ans, a[i]);
	}
	if (ans == 100005)
		cout << -1 << "\n";
	else
		cout << ans << "\n";
	return 0;
}

提交记录

C

题目传送门

算法:贪心

证明

如果你要上美术课,那么应该尽可能早。你越早提高技能,你从技能提高中受益的剩余天数就越多。

那为什么不在之后的时间上课呢?

假设你有一个解决方案,你跳过一些美术课,然后再上一些。在这种情况下,你总是可以通过提前一些较晚的美术课来提高你的分数。
换句话说,最佳解决方案总是类似于:“在某个“结束日期”之前参加每一门美术课,然后在所有剩余的日子里画画”。

通过上述,可以确定贪心算法。

实现

对于每个连续上课的结束日,你可以用 \(O(N)\) 循环模拟你赚了多少钱。由于有 \(N\) 个可能的结束日,这将花费 \(O(N^2)\) 时间。

但是,会超时。

我们不必为每个新的结束日重新模拟时间,只需记住前一天的信息即可。(记忆化???)

循环每一天,假设你正在参加所有可能的艺术课程,直到这一结束日。

然后,你可以通过将剩余天数乘以你当前的技能,并将其加到你当前的金额上,计算出你最后能赚到的钱数。

答案是你在所有可能的结束日中可以赚到的最多钱。

代码

#include <bits/stdc++.h>
using namespace std;
 
int N;
char days[100005];
 
int main () 
{
    freopen("bankin.txt", "r", stdin);
    freopen("bankout.txt", "w", stdout);
    
    cin >> N;
    cin >> days;
 
    int best = N; 
    int money = 0;
    int skill = 1;
    for (int i = 0; i < N; i++)
    {
        if (days[i] == 'C')
            skill += 1;
        else
            money += skill;
        int remaining_days = N - i - 1;
        int total_money = money + skill * remaining_days;
        best = max(best, total_money);
    }
 
    cout << best << "\n";
    return 0;
}

提交记录

D

题目传送门

暴力

对于每栋房屋,计算每家超市的劣化程度,并从所有超市中取最小值。

\(O(NM)\),能过就怪了。

正解

我们想将房屋 \(i\) 的计算工作重复用于房屋 \(i+1\),但绝对值使这变得困难。我们将问题分解为以下几个部分:找到左边最好的超市,以及找到每栋房子 \(i\) 右边最好的超市。

  • 如果超市 \(j\) 在房子的左边,那么它的坏度由 \(H_i - S_j + P_j\) 给出。
  • 如果超市 \(j\) 在房子的右边,那么它的坏度由 \(S_j - H_i + P_j\) 给出。

换句话说,对于任何给定的房子,我们要说的是:

  • 左边最好的超市是最小化 \(P_j - S_j\) 的超市。
  • 右边最好的超市是最小化 \(P_j + S_j\) 的超市。

我们将表示一些新值,以帮助简化符号。

  • \(P'_j = P_j-S_j\)
  • \(P''_j = P_j+S_j\)

因此,对于给定的房子,我们希望在左侧的所有超市中找到最小的 \(P'_j\),并在右侧的所有超市中找到最小的 \(P''_j\)

我们可以使用前缀和/后缀和数组来实现这一点。

在这种情况下,前缀最小数组在其第 \(i\) 个位置存储超市 \(i\) 之前(包括超市 \(i\))的所有超市的 \(P'_j\) 值的最小值。

类似地,后缀最小数组在其第 \(i\) 个位置存储超市 \(i\) 之后(包括超市 \(i\))的所有超市的 \(P''j\) 值的最小值。

这两个都可以在 \(O(M)\) 时间内计算出来。

然后呢?(没了。。。)

没了个锤子!


(华丽的分界线)

为了使用这些数组,我们还要知道左/右最近的超市。

可以使用双指针

让我们循环遍历每个房子 \(i\),并在遍历过程中维护一个指针 \(j\),指向最靠近房子 \(i\) 右侧的当前超市。

每当我们看到超市 \(j\) 在房子 \(i\) 的左侧时,我们就可以继续增加 \(j\),直到超市再次出现在右侧。这一定是右侧最近的超市,所以我们可以记下这个值并转到下一所房子。

当然,左侧最近的超市就是右侧最近超市左侧的超市。

要计算一所房子的最佳超市,我们现在可以查找其左侧和右侧最近的超市,并使用它们索引我们的前缀和后缀最小数组,以查询左侧和右侧的最小值。

时间复杂度 \(O(1)\)


(华丽的分界线2)

总运行时间为 \(O(N+M)\)

(我真是天才!)

(天才个der,都来看题解了还天才)

(代码都不给,有啥用?)

(。。。)


(华丽的分界线3)

代码

#include <bits/stdc++.h>
using namespace std;
 
int n, m;
int h[100005], s[100005], p[100005];
int l[100005], r[100005];
int pre[100005], suf[100005];
int sr[100005];
int ans[100005];
 
int main()
{
	freopen("shopin.txt", "r", stdin);
	freopen("shopout.txt", "w", stdout);
	
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
		cin >> h[i];
	for (int i = 1; i <= m; i++)
		cin >> s[i];
	for (int i = 1; i <= m; i++)
		cin >> p[i];
	
	for (int i = 1; i <= m; i++)
	{
		l[i] = p[i] - s[i];
		r[i] = p[i] + s[i];
	}
 
	pre[1] = l[1];
	for (int i = 2; i <= m; i++)
		pre[i] = min(pre[i - 1], l[i]);
	suf[m] = r[m];	
	for (int i = m - 1; i >= 1; i--)
		suf[i] = min(suf[i + 1], r[i]);
	int j = 1;
	for (int i = 1; i <= n; i++)
	{
		while (j <= m && s[j] < h[i])
			j++;
		sr[i] = j;		
	}
	for (int i = 1; i <= n; i++)
	{
        int best_left = INT_MAX;
        if (sr[i] > 1) {
            best_left = pre[sr[i]-1] + h[i];
        } 
        int best_right = INT_MAX;
        if (sr[i] <= m) {
            best_right = suf[sr[i]] - h[i];
        }
        int best = min(best_left, best_right); 
        ans[i] = best;
    }
    for (int i = 1; i <= n; i++)
    	cout << ans[i] << " ";
    return 0;
}

提交记录

E

题目传送门

暴力思想

因为我们要让候选人 1 获胜,所以要更改选票一定要将其更改为候选人 1。

如果我们要更改候选人的选票,我们应该按成本的顺序选择选民。

考虑一个最佳解决方案。假设任何候选人(除 1 外)获得的最大选票数为 \(X\)。这必定意味着,要从原始状态到达最佳状态,必须经过两个步骤:

  1. 首先,对于任何拥有超过 \(X\) 名选民的候选人(1 除外),我们需要逐一收买他们最便宜的选民,直到他们剩下 \(X\) 名。

  2. 然后,如果候选人 1 仍未超过 \(X\),我们需要贿赂剩余的最便宜选民,直到候选人 1 达到。

因此,如果我们能够有效地计算出步骤 1 和步骤 2 的成本,那么我们可以简单地测试 \(X\) 的每个值并从中取最佳值。

显然,\(O(M^2)\),过不了啊?

(所以,讲了这么多依旧是个暴力,对吧?)

正解

考虑优化。

从其最大值 \(M\) 开始循环遍历 \(X\) 并朝着 1 前进。在此过程中,我们将维护允许我们有效地回答步骤 1 和 2 的数组。

为了处理步骤 1,当我们循环遍历 \(X\) 时,维护一个数组,该数组存储除候选人 1 之外,每个候选人的投票人数。

随着 \(X\) 的减少,我们可以反复查询该数组以找到获得最多选票的候选人,当该数字高于 \(X\) 时,我们可以继续向最便宜的人付款,并将该候选人的计数减少一。

为了找到候选人最便宜的人,我们可以为每个候选人存储一个队列。每个队列包含该候选人的选民,按成本从小到大的顺序排序。

由于集合中的插入/删除操作总数与选民数量成正比,即 \(O(M)\)。此步骤在程序的整个运行时可以执行的最大操作数为 \(O(M \log N)\)

为了处理步骤 2,首先计算让候选人 \(1\) 超过 \(X\) 所需的最少人数。我们将这个数量称为 \(K\)。然后,我们需要某种方式来存储我们在步骤 1 中尚未支付的所有剩余选民,以及快速找到 \(K\) 个最便宜选民的总和。

我们可以通过两个集合来实现这一点,我们将其称为 \(payroll\)\(pool\)

\(payroll\) 将存储我们需要赢得的 \(K\) 个最便宜的选民,而 \(pool\) 将存储我们尚未买断但可能仍希望稍后添加到我们的 \(payroll\) 中的所有剩余选民。

随着我们减少 \(X\)\(K\) 也会减少。此外,当我们在步骤 1 中付清款项时,我们还需要将他们从 \(payroll\)\(pool\) 中删除。因此,每次我们完成步骤 1 时,可能会发生两件事:

我们的 \(payroll\) 太大。(我们支付的费用超过了我们需要支付的费用)。在这种情况下,我们可以将最昂贵的人逐一移回 \(pool\)

注:由于 \(K\) 只会随着时间的推移而减少,并且每次 \(K\) 减少一时我们只需要最多移动一次,因此我们知道在程序的运行时间内我们将最多移动 \(M\) 次。这意味着处理这种情况的摊销时间为 \(O(M \log M)\)

我们的 \(payroll\) 太小。(没有足够的资金让候选人 \(1\) 获胜)。

在这种情况下,我们可以通过反复从 \(pool\) 中取出下一个最佳选民来重新填充集合,直到 \(payroll\) 再次合适。

注:选民从 \(pool\) 移动到 \(payroll\) 的最大次数等于 1 加上他们从 \(payroll\) 移回 \(pool\) 的次数。“一”部分给我们 \(O(M)\) 次移动,并且他们在所有选民中移回的次数也是 \(O(M)\)。这意味着处理这种情况也是摊销 \(O(M \log M)\)

每次我们在两个集合之间移动或删除选民时,都要跟踪 \(payroll\) 集合内人员的总成本。一旦我们的 \(payroll\) 规模得到纠正,我们可以将此成本添加到步骤 1 的成本中,以了解当前 \(X\) 值的总成本。

(讲完了。开心吧?)

(开心个der,早晕了)

(我也晕)

注:本题解的思路借鉴了原英文题解的想法,侵权请告知。

代码

#include <bits/stdc++.h>
using namespace std;
 
typedef pair<int, int> pii;
#define x first
#define y second
#define rep(i, n) for (int i = 0; i < (n); i++) 

int n, m;
 
int main()
{
	freopen("dealin.txt", "r", stdin);
	freopen("dealout.txt", "w", stdout);
	
	cin >> n >> m;
	vector<pii> voters(m);
	rep(i, m)
	{
		cin >> voters[i].y;
		voters[i].y--;
	}
	rep(i, m)
		cin >> voters[i].x;
	sort(voters.begin(), voters.end());
	
	vector<queue<int>> voters_for(n);
	int i;
	rep(i, m) {
		voters_for[voters[i].y].push(i);
	}
	
	set<pii> numvotes;
	for (int i = 1; i < n; i++) numvotes.insert({voters_for[i].size(), i});
	int numbought = 0;
    set<pii> payroll;
    set<pii> pool;
    rep(i, m) if (voters[i].y != 0) pool.insert({voters[i].x, i});
    int best = INT_MAX;
    int step1cost = 0;
    int step2cost = 0;
    for (int X = m-1; X >= 0; X--)
	{
        while (numvotes.rbegin()->x > X)
		{
            int candidate = numvotes.rbegin()->y;
            int to_buy = voters_for[candidate].front();
            voters_for[candidate].pop(); 
            int bribe_cost = voters[to_buy].x;

            numbought += 1;
            step1cost += bribe_cost; 

            pool.erase({bribe_cost, to_buy});
            if (payroll.count({bribe_cost, to_buy})) {
                payroll.erase({bribe_cost, to_buy});
                step2cost -= bribe_cost;
            }

            pii el = *numvotes.rbegin(); 
            numvotes.erase(el);
            numvotes.insert({el.x-1, el.y});
        }

        int curr_voters = voters_for[0].size() + numbought;
        int votes_required = max((X+1) - curr_voters, 0); 

        while (payroll.size() < votes_required)
		{
            int cheapest = pool.begin()->y;
            pool.erase(pool.begin());
            payroll.insert({voters[cheapest].x, cheapest});
            step2cost += voters[cheapest].x;
        } 
        while (payroll.size() > votes_required) {
            int most_expensive = payroll.rbegin()->y;
            payroll.erase(prev(payroll.end()));
            pool.insert({voters[most_expensive].x, most_expensive});
            step2cost -= voters[most_expensive].x;
        }

        int total_cost = step1cost + step2cost;
        best = min(best, total_cost);
    }

    cout << best << "\n";
    return 0;
}

提交记录

posted @ 2024-08-19 18:54  George0915  阅读(20)  评论(0)    收藏  举报