Codeforces Round 1072 (Div. 3) 树形背包|线段树二分|区间并查集维护区间合并/set维护区间分裂

A

结论
nnn个人,两队,每队2−32-32−3个人,问两队人数差的最小值?

<=3<=3<=3的话只能分一队,另一队空,特判。

否则,如果是偶数,则可以均分成两队,因为模444余222则可以最后六个人分成两个三,剩下的均分,模444余000则可以直接均分。

如果是奇数,在偶数分法的基础上,会有一队多一人

B

分类讨论 循环

计时sss分的沙漏,每kkk分钟颠倒一次,问mmm分钟后,如果不管了,多久后沙漏会停止流动

也就是问mmm分钟时还剩多少沙子在上面。注意到这个kkk分钟颠倒一次,其实有周期性,如果s<=ks<=ks<=k,那每kkk分钟,前sss分钟沙漏会完全流完。如果s>ks>ks>k,每2k2k2k一个周期,前kkk是sss流到s−ks-ks−k,后kkk是kkk流到000。

那根据s,ks,ks,k的大小,以及mmm模2k2k2k,余数是否大于kkk分讨即可。

C

记忆化搜索

一个nnn,每次对半分,问得到111最少需要操作几次?

对半分是折半的,变小的很快,爆搜即可。但分支很多,也就是状态数还是很多,是O(n)O(n)O(n)的,需要记忆化,记录得到每个数字的最小操作次数,如果当前分支的操作次数大于记录的最小次数,剪掉。

D

位运算 组合数学

一个ddd位的二进制数,每次可以减一,或者如果是偶数的话,可以除二,要在kkk次操作内变成000,问从多少个数字开始,kkk次操作内无法变成000。

从二进制的角度,这两种操作分别就是:把第000位的111删掉,右移一位。那么二进制表示的每一个111显然都要一次,并且最高位的111也要右移到第000位才能删除,所以最高位是mxmxmx的话,还要右移mxmxmx次。

现在要计算操作次数大于kkk的元素个数,肯定无法枚举ddd位的所有数。考虑枚举最高位,这样右移次数是确定的,并且最高位一定有个111,需要用一次删除,然后要让删除操作和右移次数加起来超过kkk,那么删除次数至少是k−mx−1k-mx-1k−mx−1,也就是要安排这么多个111在[0,mx−1][0,mx-1][0,mx−1]这些位上,这些位置一共mxmxmx个空位,最多安排mxmxmx个一,所以111的个数取值范围是[k−mx−1,mx−1][k-mx-1,mx-1][k−mx−1,mx−1],枚举111的个数xxx,方案数就是C(mx,x)C(mx,x)C(mx,x)

c 复制代码
void solve() {
	int n, k;
	cin >> n >> k;
 
	int mx = __lg(n);
 
	int ans = 0;
	rep(i, 1, mx - 1) {
		int cost = i + 1;
		rep(j, k - cost + 1, i) {
			ans += C(i, j);
		}
	}
	if (mx + 1 > k)ans++;
	cout << ans << '\n';
}

E

set维护区间

一个排列,对于i∈[1,n−1]i∈[1,n-1]i∈[1,n−1],问相邻元素差不小于iii的子数组个数。

子数组太多,不能直接统计。考虑一个iii的所有区间是什么样的,其实就是所有小于iii的ai−ai−1a_i-a_{i-1}ai−ai−1的位置都断开,形成若干个区间,然后这些区间内的子数组都合法。对于一个区间[l,r][l,r][l,r]子数组个数是(r−l+1)(r−l)/2(r-l+1)(r-l)/2(r−l+1)(r−l)/2

注意iii变大过程中,断开位置是在之前基础上不断变多的,因此可以从小到大枚举iii,逐步增加断开位置,更新答案的变化量。对于一个iii我们要保存所有ai−ai−1a_i-a_{i-1}ai−ai−1的位置,然后把这些位置所在的区间断开。断开时先撤销区间的答案,然后加上断开后两个新区间的答案。

注意到这个过程也可以反着来,就是区间并查集。同样也可以做。

c 复制代码
int cal(int l, int r) {
	return (r - l + 1) * (r - l) / 2;
}
void solve() {
	int n;
	cin >> n;
	vi a(n + 1);
	vvi pos(n + 1);
 
	int cur = cal(1, n);
	rep(i, 1, n) {
		cin >> a[i];
		if (i != 1) {
			pos[abs(a[i] - a[i - 1])].push_back(i);
		}
	}
 
	set<pii>s;
	s.insert({1, n});
 
	for (int i = 1; i < n; i++) {
		cout<<cur<<' ';
		for (int p : pos[i]) {
			auto it = s.lower_bound({p, p});
			--it;
			auto [l, r] = *it;
			s.erase(it);
 
			cur -= cal(l, r);
			s.insert({l, p - 1});
			s.insert({p, r});
			cur += cal(l, p - 1);
			cur += cal(p, r);
		}
	}
 
	cout << '\n';
}

F

树形背包

选若干个不相交子树,覆盖所有叶子,问选择次数能否为三的倍数?

选择次数是三的倍数,就是模333余零,可以用一个取模背包解决,所以这实际上是一个树形背包问题,每个点枚举儿子为根的子树选不选。由于模数很小,暴力复杂度很低,不需要一般树形背包的优化。

实现上有两个问题:第一是只能选不相交子树,也就是如果我们选了xxx子树,就不能选xxx子树内后代为根的子树。这一点在实现上,需要让选当前xxx的转移,和选儿子的转移时一个或的关系,也就是在儿子的转移结束后,单独进行选整个xxx子树的转移

另一点是,我们这个dpdpdp状态里,000可能是选了000个子树的初始状态,从这个角度讲应该初始化成dp0=1dp_0=1dp0=1,但也可能表示选了3k3k3k个子树。那么如果就可能出现,实际上选了3k3k3k个的情况并不存在,初始化dp0=1dp_0=1dp0=1,会让我们误认为3k3k3k的情况是可行的。

这需要我们不能无条件初始化,只有需要利用dp0dp_0dp0这个状态进行转移前,才能初始化dp0=1dp_0=1dp0=1,这样转移后dp0=1dp_0=1dp0=1一定意味着存在一个选3k3k3k个子树的方案。

c 复制代码
void solve() {
	int n;
	cin >> n;
	vvi g(n + 1);
	rep(i, 1, n - 1) {
		int u, v;
		cin >> u >> v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	vvi h(n + 1, vi(3));
	auto &&dfs = [&](auto &&dfs, int u, int f)->void{
		bool c = 0;
		for (int v : g[u]) {
			if (v == f)continue;
			dfs(dfs, v, u);
			vi nh(3);
 
			if (!c) {
				c = 1;
				h[u][0] = 1;
			}
			rep(i, 0, 2) {
				rep(j, 0, 2) {
					if (h[u][i] == 1 && h[v][j] == 1)
						nh[(i + j) % 3] |= 1;
				}
			}
			
//			rep(i,0,2){
//				cout<<h[u][i]<<' ';
//			}
			swap(nh, h[u]);
		}
		h[u][1] = 1;
	};
 
	dfs(dfs, 1, -1);
	if (h[1][0] == 1) {
		cout << "yes\n";
	} else {
		cout << "no\n";
	}
}

G

线段树二分

带修,问一个区间[l,r][l,r][l,r]是否存在ddd,满足min([l,l+d])=dmin([l,l+d])=dmin([l,l+d])=d

注意到min([l,l+d])min([l,l+d])min([l,l+d])不增,ddd单增,所以他们相等的点一个,并且不一定是整数,显然只有整数才有解。所以问题可以转化为d−min([l,l+d])d-min([l,l+d])d−min([l,l+d])这个单增的函数,在[l,r][l,r][l,r]内是否存在整点的零点

线段树二分即可。注意这不是[1,n][1,n][1,n]的线段树二分,而是部分区间的二分,需要特殊写法。仍然把区间拆成O(logn)O(log n)O(logn)个区间,然后我们的递归优先访问左儿子,相当于从左到右访问这些区间,然后把它们依次合并,如果过合并后,d−min([l,l+d])d-min([l,l+d])d−min([l,l+d])仍小于零,由于这是个单增函数,说明零点还在右边,访问右儿子。如果大于等于零了,说明零点就在这个区间内,继续向下递归,指代递归到一个叶子。

这个过程可以抽象为,我们要找到一个[l,i][l,i][l,i]这个区间都满足某个条件的,最右的iii,因此一般我们需要在递归过程中传一个引用参数,或者设置一个全局变量,维护我们目前访问到过[l,i][l,i][l,i]的结果,每次访问到一个新的区间,如果整个区间都合并了,仍然满足条件,则把当前区间合并到[l,i][l,i][l,i]内。

对于本题的话,我们就是要找到满足d−min([l,l+d])<0d-min([l,l+d])<0d−min([l,l+d])<0的最大的iii,或者d−min([l,l+d])>=0d-min([l,l+d])>=0d−min([l,l+d])>=0的最小的iii,那么维护一个[l,i][l,i][l,i]的最小值,然后用这个最小值计算d−min([l,l+d])d-min([l,l+d])d−min([l,l+d]),判断正负,作为是否递归的条件。

得到iii后,这可能已经是d−min([l,l+d])>0d-min([l,l+d])>0d−min([l,l+d])>0的点了,并不是零点,我们关心的是零点是不是整数,所以还需要检查这一点的d−min([l,l+d])d-min([l,l+d])d−min([l,l+d])值是否等于零。

c 复制代码
struct Tree {
#define ls u<<1
#define rs u<<1|1
	struct Node {
		int l, r, mx;
	} tr[N << 2];

	void pushup(int u) {
		tr[u].mx = min(tr[ls].mx, tr[rs].mx);
	}

	void build(int u, int l, int r, vi &a) {
		tr[u] = {l, r, inf};
		if (l == r) {
			tr[u].mx = a[l];
			return;
		}
		int mid = (l + r) >> 1;
		build(ls, l, mid, a);
		build(rs, mid + 1, r, a);
		pushup(u);
	}

	void modify(int u, int idx, int val) {
		if (tr[u].l == tr[u].r)	tr[u].mx = val;
		else {
			int mid = (tr[u].l + tr[u].r) >> 1;
			if (mid >= idx)	modify(ls, idx, val);
			else modify(rs, idx, val);
			pushup(u);
		}
	}

	int query(int u, int l, int r) {
		if (r < tr[u].l || tr[u].r < l)	return inf;
		if (l <= tr[u].l && tr[u].r <= r)	return  tr[u].mx;
		return min(query(ls, l, r), query(rs, l, r));
	}
	int ask(int u, int l, int r, int &s) {
		if (l <= tr[u].l && tr[u].r <= r) {
			if (tr[u].r - l - min(s, tr[u].mx) < 0) {
				s = min(s, tr[u].mx);
				return -1;
			}
			if (tr[u].l == tr[u].r) {
				return tr[u].l;
			}
		}
		int mid = (tr[u].l + tr[u].r) >> 1;
		if (l <= mid)    {
			int res = ask(ls, l, r, s);
			if (res != -1)return res;
		}
		if (r > mid)    {
			int res = ask(rs, l, r, s);
			if (res != -1)return res;
		}
		return -1;
	}

} t;

void solve() {
	int n = rd(), q = rd();


	vi a(n + 1);
	rep(i, 1, n) {
		a[i] = rd();
	}
	t.build(1, 1, n, a);

	rep(i, 1, q) {
		int op = rd();
		if (op == 1) {
			int i = rd(), x = rd();
			t.modify(1, i, x);
		} else {
			int l = rd(), r = rd();
			int s = inf;
			int pos = t.ask(1, l, r, s);
//			cout << pos << ' ' << pos - l << ' ' << t.query(1, l, pos) << ' ';
			if (t.query(1, l, pos) == pos - l) {
				cout << 1 << '\n';
			} else {
				cout << 0 << '\n';
			}
		}
	}
}
相关推荐
Xの哲學2 小时前
Linux SKB: 深入解析网络包的灵魂
linux·服务器·网络·算法·边缘计算
无限进步_2 小时前
【C语言&数据结构】二叉树遍历:从前序构建到中序输出
c语言·开发语言·数据结构·c++·算法·github·visual studio
CodeByV2 小时前
【算法题】哈希
算法·哈希算法
天赐学c语言2 小时前
1.14 - 用栈实现队列 && 对模板的理解以及模板和虚函数区别
c++·算法·leecode
高洁012 小时前
AI智能体搭建(3)
人工智能·深度学习·算法·数据挖掘·知识图谱
不知名XL2 小时前
day24 贪心算法 part02
算法·贪心算法
AI科技星2 小时前
时空几何:张祥前统一场论20核心公式深度总结
人工智能·线性代数·算法·机器学习·生活
菜鸟233号2 小时前
力扣518 零钱兑换II java实现
java·数据结构·算法·leetcode·动态规划
咋吃都不胖lyh3 小时前
Haversine 距离算法详解(零基础友好版)
线性代数·算法·机器学习