Loading

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

C

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

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

时间复杂度 \(O(n^3\log v+Q\log v)\)\(n\leq100\)\(\log v\)map 的时间复杂度。

#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。定义 \(dp_{i,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=(A_1,\ldots,A_N)\)\(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)\) 的,还是常数太大。


于是又去写分块了。

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

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

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

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

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

注意事项:

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

  • 本题值域较大,用 \(bel\) 数组需要离散化!用 map 复杂度就会变成 \(O(n\sqrt n\log n)\),会 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\) 结点。每条边 \((a_i,b_i)\) 有边权 \(s_i\),表示走过这条边需要花 \(s_i\) 元。这个人一开始有 \(0\) 元,到了一个点 \(u\),他可以进行若干次停留,每次停留收获 \(w_u\) 元。问到达 \(n\) 的最小停留次数,并将答案加上 \(2\times n-2\)。若无解输出 -1

为什么答案要加 \(2\times n-2\),因为我们只算了停留次数。但是中间行走的步数是确定的为 \(2\times n-2\)

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


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

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

答案是赚够就走,因为在 \(u\) 点多赚一点钱,不如走到 \(v\) 号点去赚,因为 \(w_u<w_v\)


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

考虑 \((f_1,g_1)\)\((f_2,g_2)\) 两种状态,若 \(f_1<f_2\)\(g_1<g_2\),此时我们怎么判断谁更优?

事实上,\((f_1,g_1)\) 只需要到达 \(v\) 后再多停留一次,所剩余的钱就比 \(g_2\) 多。

因为 \(0\leq g_1< g_2\leq w_u<w_v\)\(g_1\leq g_2\leq w_u\) 是因为赚够就走),所以 \(g_1+w_v>g_2\)。这就说明 \((f_1,g_1)\) 优于 \((f_2,g_2)\)

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

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

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

#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 @ 2024-03-11 19:33  Otue  阅读(3)  评论(0编辑  收藏  举报