比赛链接
div2一如既往的难欸。B是个前缀和加数学,一开始读假题了wa人麻了,发现读假题了人更麻了。C是个二分答案加dfs。D是位运算的题,一开始感觉很难,但是发现是思路假了,实际上并没有那么难(还是难 )。
队友题解
A. Median of an Array
题意:
您将得到一个包含 n n n 个整数的数组 a a a 。
数组 q 1 , q 2 , … , q k q_1, q_2, \ldots, q_k q1,q2,…,qk 的中值是数字 p ⌈ k 2 ⌉ p_{\lceil \frac{k}{2} \rceil} p⌈2k⌉ ,其中 p p p 是按非递减顺序排序的数组 q q q。
例如,数组 [ 9 , 5 , 1 , 2 , 6 ] [9, 5, 1, 2, 6] [9,5,1,2,6] 的中值是 5 5 5 ,在排序后的数组 [ 1 , 2 , 5 , 6 , 9 ] [1, 2, 5, 6, 9] [1,2,5,6,9] 中,索引 ⌈ 5 2 ⌉ = 3 \lceil \frac{5}{2} \rceil = 3 ⌈25⌉=3 处的数字是 5 5 5 。并且数组 [ 9 , 2 , 8 , 3 ] [9, 2, 8, 3] [9,2,8,3] 的中值是 3 3 3 ,因为在排序的数组 [ 2 , 3 , 8 , 9 ] [2, 3, 8, 9] [2,3,8,9] 中,索引 ⌈ 4 2 ⌉ = 2 \lceil \frac{4}{2} \rceil = 2 ⌈24⌉=2 处的数字是 3 3 3 。您可以选择整数 i i i ( 1 ≤ i ≤ n 1 \le i \le n 1≤i≤n ),并在一次运算中将 a i a_i ai 增加 1 1 1 。
您的任务是找到增加数组中值所需的最少操作数。
请注意,数组 a a a 不一定包含不同的数字。
思路:
如果我们给数组元素排个序,那么中值就是第 ⌈ k 2 ⌉ \lceil \frac{k}{2} \rceil ⌈2k⌉ 个数。
我们把数值相同的一串数看成一段,当我们给某个数加一的时候,相当于把这个数提到了它所在这一段的最前方。所以我们给小于 a ⌈ k 2 ⌉ a_{\lceil \frac{k}{2} \rceil} a⌈2k⌉ 的数加一是不会带来任何变化的,同理给大于 a ⌈ k 2 ⌉ a_{\lceil \frac{k}{2} \rceil} a⌈2k⌉ 的数加一。
所以我们给等于 a ⌈ k 2 ⌉ a_{\lceil \frac{k}{2} \rceil} a⌈2k⌉ 的数加一,如果要让第 ⌈ k 2 ⌉ \lceil \frac{k}{2} \rceil ⌈2k⌉ 个数发生变化,就要让这段数的前面一段全部变成 a ⌈ k 2 ⌉ + 1 a_{\lceil \frac{k}{2} \rceil}+1 a⌈2k⌉+1。
code:
#include <iostream>
#include <cstdio>
#include <map>
using namespace std;
int T,n;
int main(){
cin>>T;
while(T--){
cin>>n;
map<int,int> mp;
for(int i=1,t;i<=n;i++){
cin>>t;
mp[t]++;
}
int t=(n+1)/2;
for(auto x:mp){
if(t>x.second)t-=x.second;
else {
cout<<x.second-t+1<<endl;
break;
}
}
}
return 0;
}
B. Maximum Sum
题意:
您有一个包含 n n n 个整数的数组 a a a 。
您正好对其执行 k k k 个操作。
在一个操作中,您选择数组 a a a 的任何相邻子数组(可能为空),并将此子数组的和插入到数组中的任何位置。您的任务是在 k k k 个这样的操作之后找到数组的最大可能总和。
因为这个数字可能非常大,所以输出答案模 1 0 9 + 7 10^9 + 7 109+7 。
提醒:一个数 x x x 模 p p p 的余数是最小的非负 y y y ,使得存在整数 q q q 和 x = p ⋅ q + y x = p \cdot q + y x=p⋅q+y 。
思路:
选出一个区间的值并加入序列,那么我们肯定想要这个区间的值 x x x 最大。得到这个值后,为了让新序列新的区间值最大,那么肯定把这个新加入的数放在这个区间的旁边,这样最大区间值就变成了两倍的 x x x。以此类推,最大区间值就会变成 4 x , 8 x , 16 x , … , 2 k ∗ x 4x,8x,16x,\dots,2^{k}*x 4x,8x,16x,…,2k∗x。然后我们再把剩余部分 t o t − x tot-x tot−x 加起来,就得到了整个序列的和 t o t + ( 2 k − 1 ) ∗ x tot+(2^k-1)*x tot+(2k−1)∗x。
code:
#include <iostream>
#include <cstdio>
#include <set>
using namespace std;
typedef long long ll;
const int maxn=2e5+5;
const ll mod=1e9+7;
const ll linf=1e18;
int T,n,k;
ll a[maxn];
ll qpow(ll a,ll b){
b%=mod-1;
ll base=a%mod,ans=1;
while(b){
if(b&1){
ans=(ans*base)%mod;
}
base=(base*base)%mod;
b>>=1;
}
return ans;
}
int main(){
cin>>T;
while(T--){
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>a[i];
ll x=0;//最大区间和
ll ts=0,tsm=0;//前缀和,最小前缀和
ll tot=0;
for(int i=1;i<=n;i++){
ts+=a[i];
x=max(x,ts-tsm);
tsm=min(tsm,ts);
}
tot=ts%mod;
x%=mod;
// cout<<"&&&"<<tot<<" "<<maxx<<endl;
cout<<((tot+(qpow(2,k)-1)*x)%mod+mod)%mod<<endl;
}
return 0;
}
C. Tree Cutting
题意:
给定一棵具有 n n n 个顶点的树。
您的任务是找出最大数量 x x x ,以便可以从此树中恰好删除 k k k 条边,从而使每个剩余连通分量 † ^{\dagger} † 的大小至少为 x x x 。
† ^{\dagger} † 两个顶点 v v v 和 u u u 在同一连通分支中,如果存在任意长度 k k k 的数列 t 1 , t 2 , … , t k t_1, t_2, \ldots, t_k t1,t2,…,tk ,使得 t 1 = v t_1 = v t1=v , t k = u t_k = u tk=u ,并且对于从 1 1 1 到 k − 1 k - 1 k−1 的每个 i i i ,顶点 t i t_i ti 和 t i + 1 t_{i+1} ti+1 由一条边连接。
思路:
直接去找 x x x 没有什么头绪,应该比较难。不过当连通块的大小越小,就越有可能得到可行的删除方式使得满足条件,当 x = 1 x=1 x=1 的时候甚至可以把所有边都删掉,也就是说答案是单调的,所以尝试二分答案,对一个可能的 x x x 进行验证。
如何验证 x x x。我们想贪心地将尽可能少的 ≥ x \ge x ≥x 个相连的点取出,然后删掉与外界相连的边。对于树来说,我们就是找到一个尽可能小的满足点个数 ≥ x \ge x ≥x 子树,删掉子树的父边,把它从整棵树中脱离出去。可以想到,任何一个完整的连通块都可以通过删除子树父边的操作来得到,因此它是正确的。
code:
这里写法上默认树的根节点有个虚拟的父节点和父边,如果删除了这个父边,说明根节点所在的连通块也是一个合法的连通块。不过这样会多删一条边,所以应该是判断 c n t > k cnt>k cnt>k,而不是 c n t ≥ k cnt\ge k cnt≥k。
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1e5+5;
int T,n,k;
int head[maxn],counter;
struct edge{
int v,nxt;
}e[maxn<<1];
void add(int u,int v){
e[++counter].v=v;
e[counter].nxt=head[u];
head[u]=counter;
}
void init(){
for(int i=1;i<=n;i++)head[i]=0;
counter=0;
}
int cnt=0;
int dfs(int u,int fa,int x){
int tot=1;
for(int i=head[u],v;i;i=e[i].nxt){
v=e[i].v;
if(v==fa)continue;
tot+=dfs(v,u,x);
}
if(tot>=x){
cnt++;
return 0;
}
else return tot;
}
bool check(int x){
cnt=0;
dfs(1,-1,x);
return cnt>k;
}
int main(){
cin>>T;
while(T--){
cin>>n>>k;
init();
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
add(u,v);
add(v,u);
}
int l=1,r=n,mid;
while(l<r){
mid=(l+r+1)>>1;
if(check(mid))l=mid;
else r=mid-1;
}
cout<<l<<endl;
}
return 0;
}
D. Birthday Gift
题意:
Yarik的生日马上就要到了,Mark决定给他一个长度为 n n n 的数组 a a a 。马克知道亚里克非常喜欢按位运算,他也有一个最喜欢的数字 x x x 。
因此,Mark希望找到最大的数字 k k k ,以便可以选择数字对[ l 2 , r 2 l_2, r_2 l2,r2 ],[ l 2 , r 2 l_2, r_2 l2,r2 ], … \ldots … [ l k , r k l_k, r_k lk,rk ]:
-
l 1 = 1 l_1 = 1 l1=1
-
r k = n r_k = n rk=n
-
从 1 1 1 到 k k k 的所有 i i i ,有 l i ≤ r i l_i \le r_i li≤ri 。
-
对于从 1 1 1 到 k − 1 k - 1 k−1 的所有 i i i,有 r i + 1 = l i + 1 r_i + 1 = l_{i + 1} ri+1=li+1。
-
( a l 1 ⊕ a l 1 + 1 ⊕ … ⊕ a r 1 ) ∣ ( a l 2 ⊕ a l 2 + 1 ⊕ … ⊕ a r 2 ) ∣ … ∣ ( a l k ⊕ a l k + 1 ⊕ … ⊕ a r k ) ≤ x (a_{l_1} \oplus a_{l_1 + 1} \oplus \ldots \oplus a_{r_1}) | (a_{l_2} \oplus a_{l_2 + 1} \oplus \ldots \oplus a_{r_2}) | \ldots | (a_{l_k} \oplus a_{l_k + 1} \oplus \ldots \oplus a_{r_k}) \le x (al1⊕al1+1⊕…⊕ar1)∣(al2⊕al2+1⊕…⊕ar2)∣…∣(alk⊕alk+1⊕…⊕ark)≤x ,其中 ⊕ \oplus ⊕ 表示 按位异或 的操作
而 ∣ | ∣ 表示 按位或 的运算。
如果不存在这样的 k k k ,则输出 − 1 -1 −1 。
思路:
求区间异或和,第一想法是先预处理前缀异或和 s i s_i si,这样查询区间异或和就只需要查询左右端点即可。原式就转化成了: ( s r 1 ⊕ s l 1 − 1 ) ∣ ( s r 2 ⊕ s l 2 − 1 ) ∣ … ∣ ( s r k ⊕ s l k − 1 ) (s_{r_1}\oplus s_{l_1-1})|(s_{r_2}\oplus s_{l_2-1})|\dots|(s_{r_k}\oplus s_{l_k-1}) (sr1⊕sl1−1)∣(sr2⊕sl2−1)∣…∣(srk⊕slk−1) ( s r 1 ⊕ s 0 ) ∣ ( s r 2 ⊕ s r 1 ) ∣ … ∣ ( s n ⊕ s r k − 1 ) (s_{r_1}\oplus s_{0})|(s_{r_2}\oplus s_{r_1})|\dots|(s_{n}\oplus s_{r_{k-1}}) (sr1⊕s0)∣(sr2⊕sr1)∣…∣(sn⊕srk−1) s r 1 ∣ ( s r 2 ⊕ s r 1 ) ∣ … ∣ ( s n ⊕ s r k − 1 ) s_{r_1}|(s_{r_2}\oplus s_{r_1})|\dots|(s_{n}\oplus s_{r_{k-1}}) sr1∣(sr2⊕sr1)∣…∣(sn⊕srk−1)考虑化简上面这个式子,因为或运算有结合律,所以我们先算左边两项: s r 1 ∣ ( s r 2 ⊕ s r 1 ) s_{r_1}|(s_{r_2}\oplus s_{r_1}) sr1∣(sr2⊕sr1) = s r 1 ∣ ( s r 2 ‾ & s r 1 ∣ s r 2 & s r 1 ‾ ) =s_{r_1}|(\overline {s_{r_2}}\&s_{r_1}|s_{r_2}\&\overline{s_{r_1}}) =sr1∣(sr2&sr1∣sr2&sr1) = s r 2 ‾ & s r 1 ∣ s r 2 & s r 1 =\overline {s_{r_2}}\&s_{r_1}|s_{r_2}\&s_{r_1} =sr2&sr1∣sr2&sr1 = s r 1 ∣ s r 2 =s_{r_1}|s_{r_2} =sr1∣sr2这个式子可以这样理解: s r 1 s_{r_1} sr1 有一些二进制位为 1 1 1,而 s r 2 s_{r_2} sr2 的这些数位上被异或影响了,随后又被 按位或 全部置为了 1 1 1。实际上这个异或就没啥作用,因此 s r 1 ∣ ( s r 2 ⊕ s r 1 ) = s r 1 ∣ s r 2 s_{r_1}|(s_{r_2}\oplus s_{r_1})=s_{r_1}|s_{r_2} sr1∣(sr2⊕sr1)=sr1∣sr2。
同理,后面的式子同样这样算,整个式子我们就可以转化为: s r 1 ∣ s r 2 ∣ … ∣ s n ≤ x \color{red}s_{r_1}|s_{r_2}|\dots|s_{n}\le x sr1∣sr2∣…∣sn≤x问题就转化为了 最多取出多少数,使得按位或的结果 ≤ x \le x ≤x。注意, s n s_n sn 一定会被取出。
考虑一个数如何才小于 x x x,应该是二进制前面的一段不变,中间有一个 1 1 1 变成 0 0 0,后面随便的数 一定会小于 x x x。更广泛地说,我们只要保证这个 1 1 1 之前的所有 0 0 0 的位置和这个位置都一定是 0 0 0,那么得到的数一定是小于 x x x 的。也就是说,我们左边每个参与按位或的数都应该在上面说的该是 0 0 0 的位置一定是 0 0 0,这样就可以得到一个或起来同样 ≤ x \le x ≤x 的数。
我们枚举这个修改的 1 1 1 的位置,就可以得到哪些位置该是 0 0 0。我们只要尽可能多地将满足条件的数取出来就可以知道最多可以分成多少个区间(数的个数等于区间个数)。这个时间复杂度是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),可以通过。
code:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1e5+5;
int T,n,x;
int a[maxn],s[maxn];
int solve(){
int t=0,ans=-1;
for(int i=30;i>=0;i--){
if((x>>i&1)==0){
t|=(1<<i);
continue;
}
int cnt=0;
t|=(1<<i);
if((s[n]&t)!=0){
t^=(1<<i);
continue;
}
for(int j=1;j<=n;j++)
if((s[j]&t)==0)
cnt++;
ans=max(ans,cnt);
t^=(1<<i);
}
return ans;
}
int main(){
cin>>T;
while(T--){
cin>>n>>x;
x++;//因为上面所作的讨论都是在<x的基础上,再特判等于x太麻烦了,就直接x+1了
for(int i=1;i<=n;i++)cin>>a[i],s[i]=a[i]^s[i-1];
cout<<solve()<<endl;
}
return 0;
}
队友的写法,思路是一致的,写法上略有不同,可以对照来看。
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
#define endl '\n'
#define VI vector<int>
#define pii pair<int, int>
#define rep(i, j, k) for (int i = j; i <= k; ++i)
#define per(i, j, k) for (int i = j; i >= k; --i)
#define print(a) cout << a << endl;
#define NO return cout << "No\n", void()
#define YES return cout << "Yes\n", void()
const int inf = 0x3f3f3f3f;
const long long llinf = (long long)0x3f3f3f3f3f3f3f3f;
const long long MOD = (long long)1e9 + 7LL;
const size_t N = (size_t)1e6 + 5;
#define IO \
ios::sync_with_stdio(false); \
std::cin.tie(0); \
std::cout.tie(0)
#define debug(a) std::cerr << "\033[34m" << #a << ':' << a << "\033[0m" << endl
#define debugList(list) \
std::cerr << "\033[34m" << #list << ": ["; \
for (auto& e : list) { \
std::cerr << e << ", "; \
} \
std::cerr << "\b\b]\033[0m" << endl
#define otto auto
int in() {
int x;
cin >> x;
return x;
}
void solve(int cs) {
int n = in(), x = in();
VI a(n + 1);
for (int i = 1; i <= n; i++) {
a[i] = in();
a[i] ^= a[i - 1];
}
int ans = 0;
for (int i = 0; i <= 32; i++) {
int k = ((x >> i) & 1 ? (x - (1 << i)) | ((1 << i) - 1) : x);
int cnt = 0;
for (int j = 1; j <= n; j++) {
if (j == n) {
if ((a[j] | k) == k) {
cnt++;
ans = max(cnt, ans);
}
else continue;
}
else if ((a[j] | k) == k) {
cnt++;
}
}
}
cout << (ans ? ans : -1) << endl;
}
signed main() {
IO;
//freopen("input.txt", "r", stdin);
//freopen("output.txt", "w", stdout);
int T, TT = 1;
T = 1;
cin >> T;
while (T--) {
solve(TT++);
}
return 0;
}