Toyota Programming Contest 2024#3(AtCoder Beginner Contest 344)

C

先预处理出三个数组能拼出的数,存放到 map 中。

查询的时候只需要看这个数是否出现在 map 里即可。

时间复杂度 O(n3logv+Qlogv)n100logvmap 的时间复杂度。

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 3e5 + 5;

int n, m, l, q, a[N], b[N], c[N], d[N];
map<int, int> vis;

signed main() {
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> a[i];
	cin >> m;
	for (int i = 1; i <= m; i++) cin >> b[i];
	cin >> l;
	for (int i = 1; i <= l; i++) cin >> c[i];
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			for (int k = 1; k <= l; k++) {
				vis[a[i] + b[j] + c[k]] = 1;
			}
		}
	}
	cin >> q;
	for (int i = 1; i <= q; i++) {
		cin >> d[i];
		if (vis[d[i]]) puts("Yes");
		else puts("No");
	}
}

D

考虑 dp。定义 dpi,j 表示前 i 个组拼成所给字符串前 j 个字符的最小花费。

对第 i 个组进行扫描,看这一个字符串能不能拼上去。

比赛时没有精细实现,所以代码看看就好了:

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 3e5 + 5;

char a[N];
int m, cnt[105], dp[205][205]; 
string s[105][105]; 

signed main() {
	memset(dp, 0x3f, sizeof dp);
	dp[0][0] = 0;
	cin >> (a + 1);
	int n = strlen(a + 1); 
	cin >> m;
	for (int i = 1; i <= m; i++) {
		cin >> cnt[i];
		for (int j = 1; j <= cnt[i]; j++) cin >> s[i][j];
	}
	for (int i = 1; i <= m; i++) {
		for (int j = 1; j <= cnt[i]; j++) {
			int len = s[i][j].size();
			for (int k = 0; k <= n; k++) {
				if (k + len > n) continue;
				string tmp = "";
				for (int w = k + 1; w <= k + len; w++) tmp += a[w];
				for (int w = 0; w <= m; w++) {
					if (tmp == s[i][j] && i > w) dp[k + len][i] = min(dp[k + len][i], dp[k][w] + 1);
				}
			}
		}
	}
	int res = 2e9;
	for (int i = 1; i <= m; i++) res = min(res, dp[n][i]);
	if (res == 2e9) puts("-1");
	else cout << res << endl;
}

E

问题陈述

给你一个长度为 N 的序列 A=(A1,,AN)A 中的元素是不同的。

请按照给出的顺序处理 Q 个查询。每个查询属于以下两种类型之一:

  • 1 x y:在 A 中元素 x 后面紧接着插入 y。当给出此查询时,保证 x 存在于 A 中。
  • 2 x:从 A 中删除元素 x。当给出此查询时,保证 x 存在于 A 中。

保证在处理完每一个查询后,A 都不是空的,并且其元素都是不同的。

处理完所有查询后,打印 A

思路

这道题为什么可以用链表做呢?因为这道题只需要最后输出整个序列,而不是实时查询。题目保证 A 中的元素是不同的,这也是可以用链表的核心。

所以链表写扩展性极低,所以比赛时写的分块。为什么某某部分人都写链表呢?不言而喻。俺就不写链表!但是又想到有个 rope 的玩意,于是很开心的写出了以下代码:

#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
#include <ext/rope>
using namespace __gnu_cxx;
using namespace std;

const int N = 4e5 + 5;

int n, a[N];
rope<int> b;

signed main() {
	scanf("%d", &n); 
	b.push_back(0);
	for (int i = 1; i <= n; i++) {
		scanf("%d", &a[i]);
		b.push_back(a[i]);
	}
	int q;
	scanf("%d",&q);
	while (q--) {
		int op, x, y;
		scanf("%d%d",&op,&x);
		if (op == 1) {
			scanf("%d",&y);
			b.insert(b.find(x)+1, y);
		}
		else {
			b.erase(b.find(x), 1);
		}
	}
	int t = b.size();
	for (int i = 1; i < t; i++) printf("%d ", b.at(i));
}

这份代码只过了 10 个点,不知道是 rope 的 find 函数是 O(n) 的,还是常数太大。


于是又去写分块了。

而分块思路简单,代码好调好写,清晰明了,常数小,大部分链表代码跑不过分块。

考虑把序列分成 n 块,每块用一个 vector 动态维护,即把这个块的所有数都放到对应的 vector 里面。在某个元素后面插入一个数时,只需要在这个元素的对应块的 vector 中插入,由于每块的 vector 大小只有 n,所以插入的时间复杂度只有 O(n)。删除一个数也是同样的道理。

但是,真的能保证每个 vector 都是 n 的大小吗?如果我们在固定一个位置加进很多个数,可能对应 vector 的大小就达到了 O(n) 级别。所以我们考虑块重构。即对整个序列重新分组,使得所有 vector 的大小变平均。

什么时候可以块重构?块重构是需要消耗 O(n) 的时间的,显然不能加一个数重构一次,可以间隔 n 次重构一次,块重构复杂度 O(nn)

总的时间复杂度也是 O(nn),事实上,vector 的 insert 非常快,所以不比链表慢多少。

注意事项:

  • 题目不是在某个位置后面添加一个数,而是在某个元素后面添加一个数,所以需要开一个数组 belx 代表 x 在序列的第几个块内。

  • 本题值域较大,用 bel 数组需要离散化!用 map 复杂度就会变成 O(nnlogn),会 TLE!

// 只用了394ms
#include <bits/stdc++.h>
using namespace std;

const int N = 6e5 + 5, M = 2000;

int n, q, a[N], lx[N], rx[N], len, b[N], tot;
int tmp[N], tl, sum[M], bel[N];
vector<int> bk[M];
// bk_i 存储这个块内的数
// lx_i,rx_i存储这个块的左右端点
// bel存储这个数所在的块 

struct edge {
	int op, l, r;
}ed[N]; 

signed main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), b[++tot] = a[i];
	int sq = sqrt(n);
	len = n / sq;
	for (int i = 1; i <= n / sq; i++) lx[i] = (i - 1) * sq + 1, rx[i] = lx[i] + sq - 1;
	if (n % sq) {
		len++;
		lx[len] = rx[len - 1] + 1;
		rx[len] = n;
	}
	int cnt = 0;
	scanf("%d", &q);
	for (int i = 1; i <= q; i++) {
		scanf("%d", &ed[i].op);
		if (ed[i].op == 1) scanf("%d%d", &ed[i].l, &ed[i].r), b[++tot] = ed[i].l, b[++tot] = ed[i].r;
		else scanf("%d", &ed[i].l); 
	}
	sort(b + 1, b + tot + 1);
	tot = unique(b + 1, b + tot + 1) - b - 1;
	for (int i = 1; i <= n; i++) a[i] = lower_bound(b + 1, b + tot + 1, a[i]) - b;
	for (int i = 1; i <= q; i++) {
		if (ed[i].op == 1) ed[i].r = lower_bound(b + 1, b + tot + 1, ed[i].r) - b;
		ed[i].l = lower_bound(b + 1, b + tot + 1, ed[i].l) - b; 
	}
	for (int i = 1; i <= len; i++) {
		for (int j = lx[i]; j <= rx[i]; j++) bk[i].push_back(a[j]), bel[a[j]] = i; 
	}
	for (int cas = 1; cas <= q; cas++) {
		int op = 0, l = 0, r = 0;
		op = ed[cas].op;
		cnt++;
		if (op == 1) {
			l = ed[cas].l, r = ed[cas].r;
			int L = bel[l], lenb = bk[L].size(); // L是l所在的块 
			for (int i = 0; i < lenb; i++) {
				if (bk[L][i] == l) {
					l = i; // 扫描到l的位置 
					break;
				}
			}
			if (l == lenb - 1) bk[L].push_back(r);
			else bk[L].insert(bk[L].begin() + l + 1, r);  
			bel[r] = L;
			rx[L]++;
			for (int i = L + 1; i <= len; i++) lx[i]++, rx[i]++;
		}
		if (op == 2) {
			l = ed[cas].l;
			int L = bel[l], lenb = bk[L].size();
			for (int i = 0; i < lenb; i++) {
				if (bk[L][i] == l) {
					l = i;
					break;
				}
			}
			bel[bk[L][l]] = 0;
			bk[L].erase(bk[L].begin() + l);
			rx[L]--;
			for (int i = L + 1; i <= len; i++) lx[i]--, rx[i]--;
		}
		if (cnt >= 5 * sq) { // 一定次数后块重构 
			tl = 0;
			for (int i = 1; i <= len; i++) {
				for (auto j : bk[i]) tmp[++tl] = j;
			}
			sq = sqrt(tl);
			len = tl / sq;
			for (int i = 1; i <= tl / sq; i++) {
				lx[i] = (i - 1) * sq + 1, rx[i] = lx[i] + sq - 1; 
			}
			if (tl % sq) {
				len++;
				lx[len] = rx[len - 1] + 1;
				rx[len] = tl;
			}
			for (int i = 1; i <= len; i++) {
				bk[i].clear();
				for (int j = lx[i]; j <= rx[i]; j++) bk[i].push_back(tmp[j]), bel[tmp[j]] = i; 
			}
			cnt = 0;
		}
	}
	for (int i = 1; i <= len; i++) {
		if (bk[i].size() == 0) continue;	
		for (auto v : bk[i]) printf("%d ", b[v]);
	}
}

F

update at 2023/3/10:更新了一些正确性说明。

场切。贪心+动态规划。

先把图建出来,可以将问题转化为在图上行走,抽象为如下问题(这是原题的严格强化):

一个人在一张有向图的 1 号结点,他要去到 n 结点。每条边 (ai,bi) 有边权 si,表示走过这条边需要花 si 元。这个人一开始有 0 元,到了一个点 u,他可以进行若干次停留,每次停留收获 wu 元。问到达 n 的最小停留次数,并将答案加上 2×n2。若无解输出 -1

为什么答案要加 2×n2,因为我们只算了停留次数。但是中间行走的步数是确定的为 2×n2

考虑贪心,考虑从 u 点走到 v 点(u,v 不一定是一条边),且在 u,v 两点都赚钱停留。如果 u 停留一次赚的钱比 v 停留一次赚的钱多,那么不如不在 v 这里停留(不如在 u 点先把钱赚了)。那么就先将 wi 从小到大排序,路径中停留的点的 wi 是递增关系,如果当前点走不到比它更赚钱的点,直接走到 n 号点。


预处理出 di,j 代表 i 走到 j 的最小路径长度(边权)。

u 号点赚钱完毕走到 v 去赚钱,是把钱赚够就走(赚够代表当前的钱可以支付 du,v),还是可以在 u 号点多赚一点,并使得到达 v 号点时钱更多?

答案是赚够就走,因为在 u 点多赚一点钱,不如走到 v 号点去赚,因为 wu<wv


既然赚够就走,那么 uv 的路径,到达 v 点后有许多种状态,用二元组 (f,g) 表示。f 代表到达 v 点后停留次数,g 代表到达 v 点后所剩的钱。到底哪种方案更优?停留次数 f 最小的更优?赚的钱 g 越多更优?停留次数越少的答案越少,赚的钱越多后面就能越少停留。

考虑 (f1,g1)(f2,g2) 两种状态,若 f1<f2g1<g2,此时我们怎么判断谁更优?

事实上,(f1,g1) 只需要到达 v 后再多停留一次,所剩余的钱就比 g2 多。

因为 0g1<g2wu<wvg1g2wu 是因为赚够就走),所以 g1+wv>g2。这就说明 (f1,g1) 优于 (f2,g2)

综上:停留次数越少,方案越优。如果停留次数相等,则剩的钱越多越优。

于是直接定义 (fi,gi) 代表 i 号点的最少停留次数与最多赚钱数,按照 w 从小到大的顺序转移即可。

复杂度 O(n4)。但是,这道题不排序也是对的,可能本题是一个拓扑图的缘故。

#include <bits/stdc++.h>
using namespace std;

#define PII pair<int, int>
#define int long long
const int N = 6405;

int T, n, m, p, w[N], f[N], g[N], st[N];
// f_i,g_i 即为i号点(最少停留次数,最多赚钱数)
int dist[N][N]; 
vector<pair<int, int> > G[N];

struct edge {
	int x, id;
}a[N];
bool cmp(edge x, edge y) { return x.x < y.x; }

void dijstra(int s) {
	priority_queue<PII, vector<PII>, greater<PII> > q;
	memset(st, 0, sizeof st);
	dist[s][s] = 0; q.push({0, s});
	while (q.size()) {
		auto t = q.top();
		q.pop();
		int u = t.second;
		if (st[u]) continue;
		st[u] = 1;
		for (auto e : G[u]) {
			auto v = e.first, d = e.second;
			if (dist[s][v] > dist[s][u] + d) {
				dist[s][v] = dist[s][u] + d;
				q.push({dist[s][v], v});
			}
		}
	}
}

void change(int x, int nf, int ng) {
	if (nf < f[x]) f[x] = nf, g[x] = ng;
	else if (nf == f[x] && ng > g[x]) f[x] = nf, g[x] = ng;
}

int get(int x, int y) {
	return (x - 1) * n + y;
} 

signed main() {
	memset(dist, 0, sizeof dist);
	cin >> n; p = 0;
	for (int i = 1; i <= n * n; i++) {
		for (int j = 1; j <= n * n; j++) {
			if (i == j) dist[i][j] = 0;
			else dist[i][j] = 1e18;
		}
	}
	for (int i = 1; i <= n * n; i++) cin >> w[i], a[i].x = w[i], a[i].id = i;
	sort(a + 1, a + n * n + 1, cmp);
	a[n * n + 1].x = 0, a[n * n + 1].id = n * n; // 要加入 n 号点,因为如果一个点走不到比它更赚钱的点,直接走到 n 好点
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n - 1; j++) {
			int x;
			cin >> x;
			G[get(i, j)].push_back({get(i, j + 1), x}); 
		}
	}
	for (int i = 1; i < n; i++) {
		for (int j = 1; j <= n; j++) {
			int x;
			cin >> x;
			G[get(i, j)].push_back({get(i + 1, j), x}); 
		}
	}
	for (int i = 1; i <= n * n; i++) {
		dijstra(i); // 预处理最短路,当然,这是一个拓扑图,直接递推也可以
	}
	for (int i = 1; i <= n * n; i++) f[i] = 1e18, g[i] = 0;
	f[1] = 0, g[1] = p;
	for (int i = 1; i <= n * n; i++) {
		for (int j = i + 1; j <= n * n + 1; j++) {
			int x = a[i].id, y = a[j].id;
			if (dist[x][y] == 1e18) continue;
			if (g[x] >= dist[x][y]) change(y, f[x], g[x] - dist[x][y]); // 如果钱足够,直接走
			else {
				int t = dist[x][y] - g[x];
				int k = (t + w[x] - 1) / w[x];
				change(y, f[x] + k, g[x] + k * w[x] - dist[x][y]);
			}
		}
	}
	if (f[n * n] <= 3e12) cout << f[n * n] + 2 * n - 2 << endl;
	else puts("-1"); 
}
posted @   Otue  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
点击右上角即可分享
微信分享提示