首页 > 其他分享 >Codeforces 做题笔记

Codeforces 做题笔记

时间:2024-02-14 22:22:35浏览次数:23  
标签:begin int ll Codeforces 笔记 pos fi nw

1841E Fill the Matrix

刚开始思路错了,以为就从下往上铺

但发现要尽量让横的连续段断开的次数少,每断开一次相当于数量 \(-1\)

所以从宽度最大的矩形开始填,尽量填满

可以用 set 维护当前行的连续段,这一列遇到黑格就 split,去除宽度为 \(1\) 的,同时记录加入的时间戳来计算矩形高度

void split(ll x, ll nw)
{
	iter it = lin.upper_bound((seg){x, N, 0});
	if(it == lin.begin())	return;
	--it;
	if(it -> r < x)	return;
	ll nl = it -> l, nr = it -> r, pre = it -> tim;	lin.erase(*it);
	if(pre <= n)	squ[++cnt] = mp(nr - nl + 1, pre - nw);
	else if(nw < n)	squ[++cnt] = mp(nr - nl + 1, pre - nw - 1);
	if(nl < x - 1)	lin.insert((seg){nl, x - 1, nw});
	if(x + 1 < nr)	lin.insert((seg){x + 1, nr, nw});
}
int main()
{
	t = read();
	while(t--)
	{
		n = read();
		for(ll i = 0; i <= n; ++i)	vec[i].clear();
		lin.clear(), tot = n, ans = cnt = 0; 
		for(ll i = 1; i <= n; ++i)	a[i] = read(), vec[a[i]].pb(i);
		m = read(), lin.insert((seg){1, n, n + 1});
		for(ll i = n; i >= 0; --i)
		{
			for(ll j : vec[i])	split(j, i); 
		}
		sort(squ + 1, squ + cnt + 1);
		for(ll i = cnt; i > 0; --i)
		{
			if(m == 0)	break;
			if(m < squ[i].fi * squ[i].se)
			{
				ans += (squ[i].fi - 1) * (m / squ[i].fi) + (m % squ[i].fi ? m % squ[i].fi - 1 : 0);
				break;
			}
			ans += (squ[i].fi - 1) * squ[i].se, m -= squ[i].fi * squ[i].se; 
		}
		print(ans), putchar('\n');
	}
	return 0;
}

1842E Tenzing and Triangle

好像跟我们出题小组搬的 T4 撞 idea 了

同样,key observation 是三角形之间不交,否则可以通过调整,花费相同代价覆盖更大面积

转化为下标后,设 \(f(i)\) 表示删完前 \(i\) 行需要的最小代价

枚举第 \(i\) 行摆放的三角形到第 \(j+1\) 行,则 \(f(i)=\min_{j<i}f(j)+A(i-j)+cost(i,j)\),\(cost(i,j)\) 为 \(j+1\sim i\) 行除三角形外删除剩下点的代价

考虑优化,发现从 \(i-1\) 行到第 \(i\) 行,第 \(k\) 列上的点会对三角形开头在 \(k+1\sim i\) 行的产生贡献

线段树优化 DP,支持区间加,区间查询最小值

ll lson(ll x)	{return x << 1;}
ll rson(ll x)	{return x << 1 | 1;}
void pushup(ll x)	{sum[x] = min(sum[lson(x)], sum[rson(x)]);}
void pushdown(ll x)
{
	if(!tag[x])	return;
	sum[lson(x)] += tag[x], sum[rson(x)] += tag[x];
	tag[lson(x)] += tag[x], tag[rson(x)] += tag[x];
	tag[x] = 0; 
}
void update(ll l, ll r, ll val, ll nl, ll nr, ll p) // 区间加 
{
	if(l <= nl && nr <= r)	
	{
		sum[p] += val, tag[p] += val;
		return;
	}
	ll mid = (nl + nr) >> 1;
	pushdown(p);
	if(mid >= l)	update(l, r, val, nl, mid, lson(p));
	if(mid < r)	update(l, r, val, mid + 1, nr, rson(p));
	pushup(p);
}
ll query(ll l, ll r, ll nl, ll nr, ll p)
{
	if(l <= nl && nr <= r)	return sum[p];
	ll mid = (nl + nr) >> 1, res = inf;
	pushdown(p);
	if(mid >= l)	res = min(res, query(l, r, nl, mid, lson(p)));
	if(mid < r)	res = min(res, query(l, r, mid + 1, nr, rson(p)));
	return res;
}
int main()
{
	n = read(), k = read(), A = read();
	for(ll i = 1; i <= n; ++i)
	{
		p[i].y = read() + 1, p[i].x = k - read(), p[i].c = read();
		vec[p[i].x].pb(p[i]);
	}
	memset(f, 0x3f, sizeof(f)), f[0] = 0;
	for(ll i = 1; i <= k; ++i) // f[i]:完全删除前 i行的点的最小代价,注意到摆放的三角形一定不交 
	{
		ll tot = 0;
		for(point j : vec[i])	
		{
			if(j.y < i)	update(j.y + 1, i, j.c, 1, k + 1, 1);
			tot += j.c; // 当 j>=y 时这个点不能被新增的三角形覆盖,要加上单独删除它的代价 
		}			
		f[i] = min(query(1, i, 1, k + 1, 1) + A * i, f[i - 1] + tot); // 找以 i结尾的三角形到 j结束 
		update(i + 1, i + 1, f[i] - A * i, 1, k + 1, 1);	
	}
	printf("%lld", f[k]);
	return 0;
}

CF1839E Decreasing Game

有意思的博弈论题,但我博弈论很差

从简单情况开始考虑什么时候后手必胜

只剩一个数,先手必胜,剩下两个不同数,先手必胜,剩下两个相同数,后手必胜……

对于每次操作,两个数的减少量相同,那么,如果最开始数能划分为两个和相同的集合,则后手能保证每一步让两个集合中的数同时减少,发现最后一步必然剩下两个相同的数,后手必胜

方法也出来了,先背包找出两个集合的划分,当先手取一个集合时,后手取另一个即可

如果先手必胜,先手的博弈方法更简单:随便取

证明:

反证法,设当前数的两个集合的和分别为 \(s_1,s_2,s_1\not= s_2\),先手取了 \(s_1\) 中的 \(a\),后手取了 \(b\),假设 \(a\le b\)

  • 如果 \(b\) 在 \(s_2\) 中,则 \(s_1,s_2\) 同时 \(-a\) 必然不相等
  • 如果 \(b\) 在 \(s_1\) 中,假设 \(s_1-a-a=s_2\),则 \(s_1-a=s_2+a\),把 \(a\) 放在 \(s_2\) 就可以把数划分为两个相同的集合,与先手必胜条件矛盾
void playfirst()
{
	for(int i = 1; i <= n; ++i)
	{
		int id = 0;
		for(int j = 1; j <= n; ++j)
			if(a[j] > 0)	{cout << j << endl, id = j;	break;}
		cin >> pos;
		if(pos == 0 || pos == -1)	exit(0);
		int mn = min(a[id], a[pos]);	a[id] -= mn, a[pos] -= mn; 
	}
}
void playsecond()
{
	int nw = sum[n] / 2;
	for(int i = n; i && nw; i = fr[i][nw] - 1, nw -= a[i + 1])	
		vis[fr[i][nw]] = 1;
	for(int i = 1; i <= n; ++i)
	{
		cin >> pos;
		if(pos == 0 || pos == -1)	exit(0);
		for(int j = 1; j <= n; ++j)
			if(a[j] > 0 && vis[j] != vis[pos])
			{
				int mn = min(a[j], a[pos]);
				a[j] -= mn, a[pos] -= mn;
				cout << j << endl;	break;
			} 
	}
}
int main()
{
	cin >> n;
	for(int i = 1; i <= n; ++i)	cin >> a[i], sum[i] = sum[i - 1] + a[i];
	f[0][0] = 1;
	for(int i = 1; i <= n; ++i)
	{
		for(int j = sum[i]; j >= 0; --j)
			if(j >= a[i] && f[i - 1][j - a[i]])	f[i][j] = 1, fr[i][j] = i;
			else	f[i][j] = f[i - 1][j], fr[i][j] = fr[i - 1][j]; 
	}
	if(sum[n] % 2 == 0 && f[n][sum[n] / 2])	cout << "Second" << endl, playsecond();
	else	cout << "First" << endl, playfirst();
	return 0;
}

CF1839D Ball Sorting

放一个球,代表着可以改变序列的一段连续段,最后不动的数,一定单调递增

问题转化为在最多能删除 \(k\) 个连续段时,删除最少的数使序列单调递增

设 \(f(i,j)\) 表示强制保留第 \(i\) 个位置,删除 \(j\) 个连续段时删的最少的数

从后往前找到能直接保留的最靠前的数 \(u\),则 \(f(i,j)=\min_{u\le k< i} f(k,j)\)

否则 \(k\sim i-1\) 的这一段只能被删除,\(f(i,j)=\min_{k<u}f(k,j-1)+i-k-1\)

最后枚举最后一个被钦定保留的位置,注意判断它之后的那段必须删还是可以保留

int main()
{
	t = read();
	while(t--)
	{
		n = read(), ans = 1e9;
		for(int i = 1; i <= n; ++i)	a[i] = read();
		memset(f, 0x3f, sizeof(f)), f[0][0] = f[1][0] = 0;
		for(int i = 1; i <= n; ++i)	f[1][i] = 1, f[0][i] = 0;
		for(int i = 2; i <= n; ++i) // f[i][j]:强制当前留下的段以 i结尾,选(最多)j个连续段时选出的最小个数 
		{
			ed = 0; // 题意等价于挖掉最多 k个连续段,使剩下的数单增,让挖去的数的个数最少 
			for(int u = i - 1; u > 0; --u)
			{
				if(a[u] > a[u + 1])	{ed = u;	break;}
				for(int j = 0; j <= n; ++j)
					f[i][j] = min(f[i][j], f[u][j]); // 最后 u\sim i可以留下 
			}
			for(int u = ed; u >= 0; --u)
				if(a[u] < a[i])
					for(int j = 1; j <= n; ++j)	// 只保留 i,挖去 u+1\sim i-1 
						f[i][j] = min(f[i][j], f[u][j - 1] + i - u - 1);
		}
		for(int i = 1; i <= n; ++i)	
		{
			ans = min(ans, f[n][i]), ed = 0;
			for(int j = n - 1; j >= 0; --j) // 最后 j\sim n可以留下 
			{
				if(a[j] > a[j + 1])	{ed = j;	break;}
				ans = min(ans, f[j][i]);
			}
			for(int j = ed; j >= 0; --j)	ans = min(ans, f[j][i - 1] + n - j); // 挖去 j+1\sim n 
			print(ans), putchar(' ');
		}
		putchar('\n');
	}
	return 0;
}

CF1838D Bracket Walk

这种题没有 key observation 就直接完了……

发现如果左/右括号连续出现了 \(\ge 2\) 个,那么可以在此反复横跳增加很多个括号

否则括号序列一定左右交错,如 \(()()()()\dots\)

set 维护奇数位置上出现的右括号,以及偶数位置上出现的左括号,按下标大小排序

  • 如果 set 为空,则括号序列如上,合法
  • 如果 set 先出现了奇数位置,则这组右括号必出现 \(\ge 2\) 个且前面只有单个出现的左括号,不合法
  • 同理,如果 set 最后出现了偶数位置,则这组左括号必出现 \(\ge 2\) 个且后面只有单个出现的右括号,不合法
  • 剩下就是合法情况
int main()
{
	cin >> n >> q >> (s + 1);
	for(int i = 1; i <= n; ++i)	a[i] = (s[i] == '(') ? 0 : 1;
	if(n & 1) // 多走的步数只能是偶数,两种括号数量不可能相同,无解 
	{
		for(int i = 1; i <= q; ++i)	cin >> pos, puts("NO");
		return 0;
	}
	for(int i = 1; i <= n; ++i)	
		if(!a[i] && i % 2 == 0)	brac.insert(i);
		else if(a[i] && (i & 1))	brac.insert(i);
	for(int i = 1; i <= q; ++i) // 为什么我想不到按奇偶位置分类…… 
	{
		cin >> pos, brac.erase(pos);
		if(a[pos] == 0)
		{
			a[pos] = 1;
			if(pos & 1)	brac.insert(pos);
		}
		else
		{
			a[pos] = 0;
			if(pos % 2 == 0)	brac.insert(pos);
		}
		if(brac.empty())	puts("YES");
		else if(*brac.begin() & 1)	puts("NO");
		else if(*brac.rbegin() % 2 == 0)	puts("NO");
		else	puts("YES");
	}
	return 0;
}

CF1837F Editorial for Two

漏了一种简单情况调半天……

枚举两个人的划分点 \(i\),则变为在 \(1\sim i\) 中选 \(x\) 个,在 \(i+1\sim n\) 个中选 \(k-x\) 个,使两边和的最大值最小

想到贪心调整,每次把 \(a_i\) 放入前半部分的备选集合,如果它比前半部分的已选集合更优则调整

把 \(a_i\) 在后半部分中删除,如果在后半部分的已选集合,则多从备选集合中拿一个补上,保证时刻已选集合总大小为 \(k\)

问题是可能改动后,前半部分多选一些更优

那就直接暴力调整,如果前半部分的和更小就一直换,把后半部分已选集合中最大的拿出来,把前半部分备选集合中最小的加入

这样一共只会调整 \(O(k)\) 次,因为每次只有 \(a_i\) 在后半部分的已选集合中才会出现调整,且每次调整后半部分已选集合的大小减少

这样总共只能减少 \(O(k)\) 次

void clear()
{
	ans = inf, tota = totb = 0;
	rma.clear(), rmb.clear(), chsa.clear(), chsb.clear(); 
}
void tzh()
{
	while(!rma.empty() && !chsa.empty() && rma.begin() -> fi < chsa.rbegin() -> fi) // 记得调整 A内部! 
	{
		tota += rma.begin() -> fi - chsa.rbegin() -> fi;
		chsa.insert(*rma.begin()), rma.insert(*chsa.rbegin());
		chsa.erase(*chsa.rbegin()), rma.erase(*rma.begin());
	}
 	while(!rmb.empty() && !chsb.empty() && rmb.begin() -> fi < chsb.rbegin() -> fi)
 	{
 		totb += rmb.begin() -> fi - chsb.rbegin() -> fi;
 		chsb.insert(*rmb.begin()), rmb.insert(*chsb.rbegin());
 		chsb.erase(*chsb.rbegin()), rmb.erase(*rmb.begin());
 	}
	while(!chsb.empty() && !rma.empty()) // 调整 A,B选的数量,可以证明只会调整 O(k)次 
	{
		if(max(tota + rma.begin() -> fi, totb - chsb.rbegin() -> fi) < max(tota, totb))
		{
			chsa.insert(*rma.begin()), tota += rma.begin() -> fi, totb -= chsb.rbegin() -> fi;
			rmb.insert(*chsb.rbegin()), chsb.erase(*chsb.rbegin()), rma.erase(*rma.begin());
		}
		else	break;
	}
	while(!chsa.empty() && !rmb.empty())
	{
		if(max(totb + rmb.begin() -> fi, tota - chsa.rbegin() -> fi) < max(tota, totb))
		{
			chsb.insert(*rmb.begin()), totb += rmb.begin() -> fi, tota -= chsa.rbegin() -> fi;
			rma.insert(*chsa.rbegin()), chsa.erase(*chsa.rbegin()), rmb.erase(*rmb.begin());
		}
		else	break;
	}
}
int main()
{
	t = read();
	while(t--)
	{
		n = read(), k = read();
		clear();
		for(int i = 1; i <= n; ++i)	a[i] = read();
		for(int i = 1; i <= n; ++i)	rmb.insert(mp(a[i], i));
		for(int i = 1; i <= k; ++i)	chsb.insert(*rmb.begin()), totb += rmb.begin() -> fi, rmb.erase(*rmb.begin());
		ans = min(ans, max(tota, totb));
		for(int i = 1; i <= n; ++i) // 枚举分界点 
		{
			rma.insert(mp(a[i], i));
			if(rmb.find(mp(a[i], i)) != rmb.end())	rmb.erase(mp(a[i], i));
			else
			{
				chsb.erase(mp(a[i], i)), totb -= a[i];
				chsa.insert(*rma.begin()), tota += rma.begin() -> fi, rma.erase(*rma.begin());
			}
			tzh(), ans = min(ans, max(tota, totb));
		}
		print(ans), putchar('\n');
	}
	return 0;
}

CF1837E Playoff Fixing

某一局的比赛次序改变,则初始位置也改变

固定了某个人的初始位置,则可以事先计算出它会在哪些场次的比赛中出现,每一局中出现的人也固定了

如果发现有不同人应在同一场比赛的同一位置,冲突无解

如果不是决赛的比赛发现两个人都不再往上走,则无解

从上往下计算每一局的安排方案

  • 如果这场比赛两个都空着,则这两个人之间可以换顺序,新增的人也可以随便
  • 如果这场比赛空着的是新增的,则新增哪个人可以随便选
  • 否则不能产生额外方案

那么,这一局有 \(2^{\text{两个人可换顺序的比赛场数}}\times (\text{可选新增的人的比赛场数})!\) 种安排方法

一局中的比赛之间不能换顺序,因为它们的顺序是由上一局决定的

int main()
{
	k = read(), n = (1ll << k), pw[0] = fact[0] = 1, tim[1] = k;
	for(ll i = 1; i <= n; ++i)	pw[i] = pw[i - 1] * 2 % mod, fact[i] = fact[i - 1] * i % mod;
	for(int i = 1; i <= k; ++i)
		for(int j = (1 << (i - 1)) + 1; j <= (1 << i); ++j)	tim[j] = k - i + 1;
	for(int i = 1; i <= n; ++i)	
	{
		a[i] = read();
		if(a[i] != -1)	
		{
			ll num = (i - 1) / 2 + (1 << (k - 1)), cur = (i & 1) ^ 1;
			for(ll j = 1; j <= tim[a[i]]; cur = (num & 1), num >>= 1, ++j)	
			{
				if(book[num][cur])	{printf("0");	return 0;}
				if(j == tim[a[i]] && book[num][cur ^ 1] && tim[book[num][cur ^ 1]] <= j && j < k)
					{printf("0");	return 0;}
				book[num][cur] = a[i];
			}
		}
	}
	for(int i = 1; i <= k; ++i)
	{
		ll tot = 0, cnt = 0;
		for(int j = (1 << (i - 1)); j < (1 << i); ++j)
			if(!book[j][0] && !book[j][1])	++tot, ++cnt;
			else if((!book[j][0] && tim[book[j][1]] > k - i + 1) || (!book[j][1] && tim[book[j][0]] > k - i + 1))
				++tot;
		ans = ans * fact[tot] % mod * pw[cnt] % mod;
	}
	printf("%lld", ans);
	return 0;
}

CF1834E MEX of LCM

好题

固定左端点 \(l\),则什么时候 \(\mathrm{lcm}\) 会改变呢?

新加入的数 \(a_x\) 必定有原来 \(\mathrm{lcm}\) 中没有的质因子,最小为 \(2\)

也就是说,新的\(\mathrm{lcm}\) 至少为原来的 \(2\) 倍

于是答案有了一个上界 \(O(n\log V)\),可以从后往前找,维护不同的 \(\mathrm{lcm}\) 的数列,数列长度为 \(O(\log V)\),在 \(O(n\log V)\) 时间内找出所有的 \(\mathrm{lcm}\)

但评论区证明了更紧的上界:

考虑以 \(l\) 开头的连续段中值域在 \([X,2X)\) 中的 \(\mathrm{lcm}\),最多只会有 \(1\) 个

那么 \([N+1,2N+2)\) 中最多只能有 \(n\) 个 \(\mathrm{lcm}\),但区间内已经有了 \(N+1\) 个数,因此必然有一个空隙,答案最大为 \(2N+1\)

int main()
{
	t = read();
	while(t--)
	{
		n = read();
		for(int i = 1; i <= n; ++i)	a[i] = read();
		for(int i = 1; i <= n * 2 + 1; ++i)	book[i] = 0;
		cnt = 0;
		if(a[n] <= n * 2 + 1)	book[a[n]] = 1, f[++cnt] = a[n];
		else	f[++cnt] = 1;
		for(int i = n - 1; i > 0; --i)
		{
			for(int j = 1; j <= cnt; ++j)	lcm[j] = f[j];
			idx = 0, ggcd = a[i]; 
			for(int j = 1; j <= cnt; ++j) // 后缀长度逐渐减小,后面的 lcm 是前面的因数
			{ 
				ggcd = gcd(ggcd, lcm[j]); // 均摊求 gcd 的复杂度,因为 gcd 只会减小
				if(1ll * lcm[j] / ggcd * a[i] <= 2ll * n + 1 && lcm[j] / ggcd * a[i] != f[idx])	
					f[++idx] = lcm[j] / ggcd * a[i]; // 用后面数结尾的后缀的 lcm 递推前面数的 
			}
			if(f[idx] != a[i] && a[i] <= 2 * n + 1)	f[++idx] = a[i];
			cnt = idx;
			for(int j = 1; j <= cnt; ++j)	book[f[j]] = 1;
		}
		for(int i = 1; i <= n * 2 + 1; ++i)
			if(!book[i])
			{
				print(i), putchar('\n');
				break;
			}
	}
	return 0;
}

CF1834F Typewriter

先不考虑修改,发现若 \(p_i< i\),则一定要重置小车来还原 \(p_i\)

答案下界为 \(\sum_{i=1}^n [p_i<i]\)

构造出答案下界的方案:将大小 \(>1\) 的置换环按 \(p\) 最小值从小到大排序,对一个置换环中元素从 \(p\) 最小到最大操作,调整完这个环后回到最小点,因为最小值从小到大排序,所以可以直接切换到后一个环

再考虑每个数对答案的贡献,将左右移统一为右移,移动步数 \(\in(-n,n)\),那么每个数至多在 \(2\) 个移动步数的区间产生贡献

于是可以差分计算出对于每个步数的答案

反转就直接反转序列预处理

复杂度 \(O(n+q)\)

void work(int *sum)
{
	for(int i = 1; i <= n; ++i)
	{
		if(a[i] >= i)
		{
			if(a[i] - i + 1 <= n - i)	++sum[a[i] - i + 1], --sum[n - i + 1];
		}
		else
		{
			++sum[0], --sum[n - i + 1];
			if(n + a[i] - i + 1 <= n - 1)	 ++sum[n + a[i] - i + 1];
		}
	}
	for(int i = 1; i < n; ++i)	sum[i] += sum[i - 1];
}
int main()
{
	n = read();
	for(int i = 1; i <= n; ++i)	a[i] = read(), num += (int)(a[i] < i);
	work(cnt[0]), reverse(a + 1, a + n + 1), work(cnt[1]);
	print(cnt[0][0]), putchar('\n'), 
	q = read();
	for(int i = 1; i <= q; ++i)
	{
		op = read();
		if(op == 1)
			t = read(),	tot[cur] = (tot[cur] + n - t) % n, tot[cur ^ 1] = (tot[cur ^ 1] + t) % n;
		else if(op == 2)
			t = read(),	tot[cur] = (tot[cur] + t) % n, tot[cur ^ 1] = (tot[cur ^ 1] + n - t) % n;
		else	cur ^= 1;
		print(cnt[cur][tot[cur]]), putchar('\n');
	}
	return 0;
}

CF1855B Longest Divisors Interval

div2B 差点不会做……控诉样例解释误导人

显然 \(n\) 为奇数时答案为 \(1\),但是我研究了半天样例解释也不明白那个区间是怎么找出来的

换种思路,长度 \(\ge m\) 的区间必然有 \(m\) 的倍数,那么只要找到 \(1\sim x\) 中第一个不是 \(n\) 的因数的元素 \(x\),则答案最大为 \(x-1\),而且 \([1,x-1]\) 一定是符合要求的

我还看了半天,为什么 div2B 这么难……

CF1854A2 Dual(hard version)

CF1854B Earn or Unlock

CF1854D Michael and Hotel

CF1852C Ina of the Mountain

如果每个数的大小固定,根据经典的结论,当 \(a_i>a_{i-1}\) 时,会有 \(a_i-a_{i-1}\) 的贡献,意味着 \(a_i\) 不能顺便被前面的摆好

但是现在我们可以增加前面,当 \(a_{j-1}>a_j\) 时,花费的代价是 \(a_j+k-a_{j-1}\),把 \(j\sim i-1\) 在原来的基础上增加 \(k\),否则为 \(a_j-a_{j-1}\),让前面的元素比 \(a_i\) 大,使 \(a_i\) 不再产生贡献,且对 \(i\) 之后的元素没有影响

每个数最多增加一次,差超过 \(k\) 肯定不优

贪心地用小根堆维护代价,每次遇到 \(a_i\le a_{i-1}\) 时,往堆中加入 \(a_i+k-a_{i-1}\),否则加入 \(a_{i}-a_{i-1}\),之后取出堆顶,作为当前调整的代价

CF1852D Miriany and Matchstick

CF1852B Imbalanced Arrays

CF1473E Minimum Path

图论转化

这个边的最大,最小值肯定不好记录,一记下就直接炸了

但是想让路径长度最小,发现是去掉一条最长的边又增加一条最短的边

如果我们可以自己指定删除和增加的边,发现刚好与要求吻合

因此,直接记录 \(f(i,0/1/2/3)\) 表示到 \(i\) 点,不修改/增加一条/删除一条/增加且删除一条的最短路,分层图

对于这种把限制转化掉的题目,见的不少了,下次会做吗?

void add(ll u, ll v, ll w)
{
	edge[u].pb(mp(v, w)), edge[u].pb(mp(v + n, 0)), edge[u].pb(mp(v + n * 2, w + w)), edge[u].pb(mp(v + n * 3, w));
	edge[u + n].pb(mp(v + n, w)), edge[u + n].pb(mp(v + n * 3, w * 2));
	edge[u + n * 2].pb(mp(v + n * 2, w)), edge[u + n * 2].pb(mp(v + n * 3, 0));
	edge[u + n * 3].pb(mp(v + n * 3, w));
}

CF1801C Music Festival

其实可以直接暴力一点(

把原序列的每个可能产生贡献的子序列拿出来,它一定是到末尾,总个数为 \(O(\sum k_i)\)

这时就必须要求子序列选完了,把它们按最小的元素升序排序,设以子序列 \(i\) 结尾时的最大权值为 \(f_i\),则 \(f_i=\max_{r_j<l_i}\{f_j+len_i\}\)

然后用树状数组优化

其实还可以把每个 \(a_i\) 处存下哪些序列中包含它,然后递增的考虑每个元素,含有它的序列权值可以直接在原来的基础上增加一个,也可以把它接在前面某个完整序列的末尾,表示这个序列拼接在它后面

注意清空的复杂度

int main()
{
 	t = read();
 	while(t--) // 多测清太多,爆零两行泪 
 	{
 		n = read();
 		lin.clear(); 
// 		for(reg int i = 1; i <= cnt; ++i)	lin[i].clear(); 不要这样写!多测如果数据有很多组会 g! 
 		ans = cnt = sum = tot = 0;
 		for(reg int i = 1; i <= n; ++i)
 		{
 			k[i] = read(), a.clear();
 			for(reg int j = 1; j <= k[i]; ++j)	a.pb(read());
 			last = f[i] = mx[i] = 0;
 			for(reg int j = 0; j < k[i]; ++j)
 				if(a[j] > last)	lin[a[j]].pb(i), last = a[j];
			cnt = max(cnt, last), mx[i] = last;
	    }
	 	for(reg map<int, vector<int> >::iterator it = lin.begin(); it != lin.end(); ++it)
		{
			c = it -> first, a = it -> second;
			tot = 0;
			for(reg int i : a) // 看当前值 c有哪些段中有它 
			{
				f[i] = max(f[i] + 1, sum + 1); // 当前可以往这段之前接,也可以直接在另外的段后面 
				if(c == mx[i])	tot = max(tot, f[i]);
				// 有一段结尾了,可以更新当前已经结束的段的最大权值 
			}
			sum = max(sum, tot); // 更新当前已经结束的段的最大值 
	    }
	    for(reg int i = 1; i <= n; ++i)	ans = max(ans, f[i]);
		print(ans), putchar('\n'); 
    }
	return 0;
}

CF1801D The way home

又是边跑最短路边 DP

首先肯定想尽量在 \(w\) 大的地方演出,问题是不知道后面的花费,就不知道应该在当前演出多少场

但为什么一定要在那时才演出呢?这完全等价与先走,等钱不够时,从经过的地方中找 \(w\) 最大的再演出

代价延迟计算的思想很重要

跑最短路的同时记录经过的 \(w\) 最大的点和已经演出的次数与剩余钱数,优先从堆中取出演出次数少,然后是剩余钱数多的状态

发现钱不够了,就计算额外演出的次数

struct node
{
	ll mny, tim, id, best;
	inline void init()	{tim = inf, mny = id = best = 0;}
	bool operator <(const node &c)const // 堆中重载运算符,优先考虑当前次数少且钱多的状态 
	{
		if(tim != c.tim)	return tim > c.tim;
		if(mny != c.mny)	return mny < c.mny;
	}
	bool operator !=(const node &c)const
	{
		return mny != c.mny || tim != c.tim;
	}
}f[810][810];
inline node mn(node a, node b)
{
	if(a.tim < b.tim || (a.tim == b.tim && a.mny > b.mny))	return a;
	return b;
}
inline void dij()
{
	priority_queue<node> heap;
	for(reg ll i = 1; i <= n; ++i)
		for(reg ll j = 1; j <= n; ++j)	f[i][j].init();
	f[1][1] = (node){p, 0, 1, 1}, heap.push(f[1][1]);
	while(!heap.empty()) // 用 dij 的思想在图上 DP 
	{
		node t = heap.top();	heap.pop();
		if(f[t.id][t.best] != t)	continue; // 不是这个状态的最优,跳过 
		for(reg pll i : edge[t.id])
		{
			if(i.se > t.mny) // 需要额外表演,但最核心的想法是表演可以看作是钱不够了再在前面能拿更多钱的地方表演,延后计算 
			{
				ll nw = t.best, ci = (i.se - t.mny + w[nw] - 1) / w[nw], res = ci * w[nw] + t.mny - i.se;
				if(w[i.fi] > w[nw])	nw = i.fi;
				heap.push((node){res, t.tim + ci, i.fi, nw});
				f[i.fi][nw] = mn(f[i.fi][nw], (node){res, t.tim + ci, i.fi, nw});
			}
			else
			{
				ll nw = (w[i.fi] > w[t.best]) ? i.fi : t.best;
				heap.push((node){t.mny - i.se, t.tim, i.fi, nw});
				f[i.fi][nw] = mn(f[i.fi][nw], (node){t.mny - i.se, t.tim, i.fi, nw});
			}
		}
	}
}

标签:begin,int,ll,Codeforces,笔记,pos,fi,nw
From: https://www.cnblogs.com/KellyWLJ/p/18015700

相关文章

  • Codeforces Round 925 (Div. 3)
    A简单分讨。最前面a能放多少就放多少,大头尽量放在后面。B先算出每个水缸最终的水量,然后从前往后扫,多的水平到下一个水缸里去。假如扫到一个水缸小于平均值,那么没救了,输出NO。CC<<B。考虑全体值为\(a_1\)与\(a_n\)时的最小代价,搞两个指针,从前后开始扫一扫即可。D......
  • Codeforces Round 925 (Div. 3) 赛后总结
    此次总结借鉴了Register_int,0x3ea,幻想家协会会长的题解。感谢大佬。RecoveringaSmallString题目大意:将字母a-z编号为1-26,给出一个整数,此整数为三个字母之和,求改字符串的最小字典序。分析可以暴力循环,或者分情况讨论.我们只要尽力保持越前面的字母越小越好。#include<i......
  • 【文化课学习笔记】【化学】选必一:水溶液中的离子反应与平衡(下)
    【化学】选必一:水溶液中的离子反应与平衡(下)盐类水解基本概念定义:盐电离出的离子与水电离出的\(\ce{H+}\)或\(\ce{OH-}\)相互作用生成弱电解质的反应。特点:可逆:水解是可逆反应,在一定条件下达到化学平衡。程度小:通常盐类水解程度很小,一般无沉淀析出、无气体放出。例......
  • 【学习笔记】关于图论的一切
    存图邻接矩阵边集邻接表最小生成树primkruskal最短路dij堆优化spfafloyd欧拉路欧拉回路scc缩点2-sAT二分图基础概念匈牙利DAG最小链覆盖网络流Dinic最小割最大权闭合子图最小割集费用流Zkw双连通问题割边割点双连通分量圆方树生成树计数......
  • KTT学习笔记
    KTT学习笔记KTT是由EI给出的解决区间加正数、区间最大子段和的数据结构。大体的思路是在把普通最大子段和的信息看成和增量有关的一次函数,然后维护增量为多少时取到最大值的信息会改变,相当于是维护凸壳,但是只维护当前段和当前段的末尾位置,通过势能分析可以得到复杂度是\(O(......
  • 卡方分布笔记
    Thechi-squaredistributionisacontinuousprobabilitydistributionthatiswidelyusedinstatisticalinference,particularlyinthecontextofhypothesistestingandintheconstructionofconfidenceintervals.Itarisesprimarilyinthecontextofest......
  • Codeforces Round 925 (Div. 3)
    ABC都是一眼题,用了16min。D没有一眼看出来,先往后看。E是博弈论直接跳过。F一眼出思路。G一开始都错题了,不会做。遂先写F。发现我不会判断图中是否有环。一开始写了个dfs以为能过结果复杂度是\(\mathcalO(n^2)\)的,罚时+1。想改成DP那样的记忆化发现很假。于是b......
  • 莫队学习笔记
    莫队莫队是一种常见的离线处理区间查询问的方法。莫队的思想是把序列分块,然后把询问按照左端点所在块为第一关键字,右端点为第二关键字排序,然后处理询问,维护指针\(l,r\)表示当前处理的区间是\([l,r]\),每次根据询问区间来移动指针计算贡献。关于复杂度。假设指针移动的复杂度......
  • Codeforces Round 925 (Div. 3)
    CodeforcesRound925(Div.3)A-RecoveringaSmallString解题思路:枚举.代码:#include<bits/stdc++.h>usingnamespacestd;usingll=longlong;usingpii=pair<ll,ll>;#definefifirst#definesesecondusingi128=__int128_t;usingpiii=pa......
  • Go语言精进之路读书笔记第26条——了解接口类型变量的内部表示
    接口是Go这门静态语言中唯一“动静兼备”的语言特性接口的静态特性接口类型变量具有静态类型,比如:vareerror中变量e的静态类型为error支持在编译阶段的类型检查:当一个接口类型变量被赋值时,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法接口的动态特性接......