A. Bitwise Operation Wizard(交互+位运算)

官方评分:*1700

个人评分:*1600

代码难度并不高,比较偏向思维和技巧,相当好玩

题意

    有t组输入,每次给定一个长度为n的排列,里面只包含0~n-1,每个数恰好只出现一次,且顺序不定。我们需要找到两个下标i和j,使得的值最大化.
    本题为交互题,每次询问的格式为”? a b c d”,最后会返回比较符号”<”或”=”或”>”,如果返回”<”,则说明,”=”和”>”同理。最终我们通过推理得到的答案用”! i j”的格式输出

数据范围

  • 询问次数不超过

思路

省流:找最大值下标 i(n-1次查询)+存”互补”数下标(n-1次查询)+找”互补”数中最小值下标 j(不超过n-1次查询)

详解

    位运算一定要拆位去考虑每个独立的二进制位对答案的贡献,不难想到,两个数的二进制位必须互不相同,如,这样异或得到的最大值一定是形如“”,即每个二进制位都是1.

  1. 对于每次询问,我们询问出的答案只有大小关系而非具体的值,而且是或运算( | )的形式;由于每个数或上自己一定等于自己(x | x = x),所以我们从第一数开始两两比较,每次记录下当前的最大值下标并更新,持续进行遍历询问,可以通过n-1次询问找到最大值也就是n-1的下标.不难想到,两数异或值最大,其中一个数必然可以是n-1,因为n-1一定包含了中最前面的1。所以现在的任务就是找到所有满足与n-1二进制值恰好“互补”的数的下标。
  2. 对于询问”? a b c d”,我们将a,c设为最大值下标,b设为当前最符合互补的数的下标,d设为i进行遍历,如果出现小于,则说明当前的数一定不互补,每次更新时b和d的值,例如当n=9时显然,假如,此时假如我们搜到的值为,那么,所以此时搜到的值一定是更加符合互补的值,将其更新到b;不难想到,最后一次出现”<”,说明当前和往后所有出现”=”的数,一定是互补的.例如当为最大值,那么最后一次出现”<”和往后的”=”一定是形如的二进制值!
  3. 对于所有的”互补”数字,我们需要找到与n-1恰好完全“互补”的数,如上的形式,因为位为X时n-1位已经为1,那么X必须取0不难想到,当X均取0时,就是所有满足互补的数中的最小值!

上述三段操作,每次遍历操作均不超过n次查询,所以最后的总查询次数一定不超过次。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include<bits/stdc++.h>
#define int long long
//#define endl '\n' 交互题存在缓存区问题,一定不能用'\n'!!!
using namespace std;
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
int t;cin >> t;
while(t--){
int n, max_id, min_id;
cin >> n;
if(n==2){//如果只有两个数直接特判即可
cout << "! 0 1" << endl;
fflush(stdout);
continue;
}
cout << "? 0 0 1 1" << endl;
fflush(stdout);
auto ask1 = [&](int x, int y) -> int { //比较返回最大值
char ch;
cin >> ch;
return (ch == '>' ? x : y);
};
auto ask2 = [&](int x, int y) -> int{ //比较返回最小值
char ch;
cin >> ch;
return (ch == '<' ? x : y);
};
//找最大值n-1的下标
max_id = ask1(0, 1);
for (int i = 2; i < n;i++){
cout << "? " << max_id << ' ' << max_id << ' ' << i << ' ' << i << endl;
fflush(stdout);
max_id = ask1(max_id, i);
}
int a, b, c, d;
a = b = c = max_id;
vector<int> stk; //存放“互补”数(不一定完全)的下标
for (int i = 0; i < n;i++){ //遍历找到与n-1“互补”的下标
if(i==max_id)
continue;
d = i;
cout << "? " << a << ' ' << b << ' ' << c << ' ' << d << endl;
fflush(stdout);
char ch;
cin >> ch;
if(ch=='<'){ //说明往后搜到的数更符合"互补",清空之前容器中存放的下标
b = i;
stk.clear();
stk.emplace_back(i);
}else if(ch=='='){ //将说明当前的数也能满足"互补",加入容器中
stk.emplace_back(i);
}
}
int num = stk.size();
if(num==1){ //对只有一个满足“互补”的情况进行特判,如"1000"只和"0111"满足互补
min_id = stk[0];
cout << "! " << max_id << ' ' << min_id << endl;
continue;
}
//寻找“互补”的最小值的下标,即满足"完全互补"的数的下标
cout << "? " << stk[0] << ' ' << stk[0] << ' ' << stk[1] << ' ' << stk[1] << endl;
fflush(stdout);
min_id = ask2(stk[0], stk[1]);
for (int i = 2; i < num;i++){
cout << "? " << min_id << ' ' << min_id << ' ' << stk[i] << ' ' << stk[i] << endl;
fflush(stdout);
min_id = ask2(min_id, stk[i]);
}
//答案就是max_id与min_id
cout << "! " << max_id << ' ' << min_id << endl;
fflush(stdout);
}
return 0;
}

B. Beautiful Triple Pairs(桶计数+简单容斥)

官方评分:暂无

个人评分:*1400

摘自最近的一场div.3的c,感觉难度上确实超过了很多3d

题意

给定一个n个数的数组a,选择连续三个值为一组三元组,形式化的,三元组应为,其中。我们定义两组三元组b,c当满足以下任一情况时为“漂亮对”:

  • and and
  • and and
  • and and
    也就是说三元组恰好只有一个元素不同,求数组中“漂亮对”的数量。

数据范围

思路

因为求的是数对,很明显的套路就是遍历数组并使用桶存储数量,边遍历边统计。因为有三种情况都能满足“漂亮对”,所以我们开三个map记录,cnt_12表示第1,2个元素相同的数量,cnt13和cnt23同理,最后再开个桶mp记录当前三元组的数量,需要减去当前三元组的数量以保证一种元素不同,例如当前三元组为[1,2,2],则后面满足条件的三元组一定是(cnt12[{1,2}]-mp[{1,2,2}])+(cnt13[{1,2}]-mp[{1,2,2}])+(cnt23[{2,2}]-mp[{1,2,2}]),每种情况都需要减去mp[{1,2,2}],因为需保证至少一种元素不同.

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
typedef pair<int,int>pii;
typedef pair<int, pii> PII;
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
int t;cin >> t;
while(t--){
map<pii, int> cnt12, cnt13, cnt23;
map<PII, int> mp;
int n;cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n;i++)cin >> a[i];
int ans = 0;
for (int i = n - 2; i >= 1;i--){
ans += cnt12[{a[i], a[i + 1]}] + cnt13[{a[i], a[i + 2]}] + cnt23[{a[i + 1], a[i + 2]}];
PII temp;
temp.first = a[i], temp.second.first = a[i + 1], temp.second.second = a[i + 2];
ans -= 3 * mp[temp]; //每种情况都要减
//边遍历边统计
cnt12[{a[i], a[i + 1]}]++, cnt13[{a[i], a[i + 2]}]++, cnt23[{a[i + 1], a[i + 2]}]++;
mp[temp]++;
}
cout << ans << endl;
}
return 0;
}

C. Nene’s Magical Matrix(构造)

官方评分:*1600

个人评分:*1400

构造题,只要能想出结论秒切

题意

有一个的网格,我们可以使用横向或纵向放置一个任意序列的排列p,最多进行有次操作,最大化的值.

数据范围

思路

总共n行n列,进行最多2n次操作,根据直觉,可以n行n列开始从大到小行列开始交替进行n次操作,恰好2n次操作,最后一定能构造出形如

的矩阵,粗略地证明该构造方式的正确性:显然,通过多次的覆盖,的最大值一定为,所以以上的构造方式理论上满足的最大值.。当时,直接根据题中样例打表即可.

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
int t;cin>>t;
while(t--){
int n;cin>>n;
if(n==1){
cout<<"1 1"<<endl;
cout<<"1 1 1"<<endl;
}else if(n==2){
cout<<"7 3"<<endl;
cout<<"1 1 1 2"<<endl;
cout<<"1 2 1 2"<<endl;
cout<<"2 1 1 2"<<endl;
}else{
int sum=0;
for(int i=1;i<=n;i++)
sum+=(2*i-1)*i;
cout<<sum<<' '<<2*n<<endl;
for(int i=1;i<=n;i++){
cout<<"1 "<<n-i+1<<' ';
for(int j=1;j<=n;j++)
cout<<j<<' ';
cout<<endl;
cout<<"2 "<<n-i+1<<' ';
for(int j=1;j<=n;j++)
cout<<j<<' ';
cout<<endl;
}
}
}
return 0;
}

D. Progressive Square(模拟)

签到题,用map记录数量,第一行第一个元素为所有最小值,遍历时检查是否存在该矩阵即可;很难理解,赛时为什么一直没有几个人发现这道题甚至最后无人AC.

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
int a[250010],s[505][505];
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
int T;cin>>T;
while(T--){
int n, c, d;cin >> n >> c >> d;
map<int, int> mp;
for (int i = 1; i <= n * n; i++){
cin >> a[i];
mp[a[i]]++;
}
int mini = *min_element(a + 1, a + n * n + 1);
mp[mini]--, s[1][1] = mini;
auto check = [&]() -> bool{
for (int i = 1; i <= n;i++)
for (int j = 1; j <= n;j++)
if(i==1&&j==1)
continue;
else if(j>1){
if(mp[s[i][j-1]+d]>0)
mp[s[i][j - 1] + d]--, s[i][j] = s[i][j - 1] + d;
else
return 0;
}else{
if (mp[s[i-1][j] + c] > 0)
mp[s[i - 1][j] + c]--, s[i][j] = s[i - 1][j] + c;
else
return 0;
}
return 1;
};
cout << (check() ? "YES" : "NO") << endl;
}
return 0;
}

E. Vlad and Division(位运算)

题意

    给定长度为n的正整数数列,如果两个数的二进制每一位均不相同,形式化的,对于x和y,,其中,那么可以将其分为一组,求将原数组所有数分组的最小组数.

数据范围

思路

    处于同一组的元素x和y,两两之间的位均不相同,不难想到x + y = 1 << 31 - 1,所以我们开一个map记录某数是否出现过,当遍历的数为x时,且mp[x]>0,此时如果mp[(1LL << 31-1)-x]>0,那么此时两者一定能分为一组,然后将map的数量值减一,记得1后面最好要加LL强制转换成long long类型,不然可能会溢出影响最终答案!

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
int a[200010];
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
int T;cin>>T;
while(T--){
int n;cin>>n;
map<int,int>mp;
for(int i=1;i<=n;i++)
cin>>a[i],mp[a[i]]++;
int tot=0;
for(int i=1;i<=n;i++){
if(mp[a[i]]){
mp[a[i]]--;
if(mp[(1LL<<31)-1-a[i]])mp[(1LL<<31)-1-a[i]]--;
tot++;
}
}
cout<<tot<<endl;
}
return 0;
}

F. Chat Screenshots(拓扑排序)

官方评分:*1700

个人评分:*1400

摘自一套div.3的F题,其实就是一个板的不能再板的模板题,洛谷评分也仅为黄题

前置知识:拓扑排序

题意

    有n个人在屏幕前聊天,每一个人都把自己列为顶部且能看见其他人的先后顺序。给出k个人电脑上看到的先后顺序,判断所有人是否存在严格的顺序关系。

数据范围

思路

    因为本题聚焦的是前后关系,很容易想到有向无环图(DAG),也就是经过k个人的前驱关系,能否建立一个DAG,那么,这题就演变成了拓扑排序并判断有无环的模板题。当然,为防止重复建边,可用map或set进行记录,当数量为零时才能够存图建边。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
typedef pair<int,int>pii;
int n,in[200010];
vector<int>e[200010];
bool toposort(){ //拓扑排序判环
queue<int>q;
int tot=0;
for(int i=1;i<=n;i++)
if(!in[i])q.push(i),tot++;
while(!q.empty()){
int temp=q.front();
q.pop();
for(auto it:e[temp]){
in[it]--;
if(!in[it])q.push(it),tot++;
}
}
if(tot==n)return 1;
else return 0;
}
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
int T;T=1;
cin>>T;
while(T--){
int k;cin>>n>>k;
set<pii>st;
for(int i=1;i<=k;i++){
int y;
for(int j=1;j<=n;j++){ //建边
int x;cin>>x;
if(j==1)continue;
if(j>2){
int num1=st.size();
st.insert({y,x});
int num2=st.size();
if(num1!=num2) //防止重复建边
e[y].push_back(x),in[x]++;
}
y=x;
}
}
if(toposort())cout<<"YES"<<endl;
else cout<<"NO"<<endl;
//最后每次都记得清空数组和容器
for(int i=1;i<=n;i++)in[i]=0,e[i].clear();
}
return 0;
}

G. Destroying Bridges

    签到题,随手画个图可知,当k小于n-1时,可访问所有岛屿,否则只能访问一个.

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
int T;cin >> T;
while (T--){
int n, k;
cin >> n >> k;
if (k < n - 1) cout << n << endl;
else cout << "1" << endl;
}
return 0;
}

H. Long Inversions(暴力+树状数组)

官方评分:*1700

个人评分:*1500

前置知识:树状数组

题意

    给出长度为 的二进制字符串 。二进制字符串是仅由字符“1”和“0”组成的字符串。选择一个整数 ( ),然后多次应用以下操作:选择字符串中的 k 个连续字符并将它们反转,即将所有“0”替换为“1”,反之亦然。使用任意次操作,使字符串中的所有字符等于“1”。求出可以使字符串中的所有字符等于“1”的k的最大值

数据范围

思路

    从暴力的思路看,很明显能得到,遍历当前字符串,如果当前字符为‘1’,那么直接跳过即可;如果当前字符为‘0’,则接下来k个字符进行翻转,这样暴力修改可以使得s.length()-k之前的字符都变为1。接下来只需判断的字符是否全为1或0即可。
    如果是暴力修改(O())和暴力找最大值(O())的话,最坏情况的复杂度是O(),则本题显然会TLE;所以需要对复杂度进行优化。
    注意到,每次修改操作是区间变化,并且还需要时刻查询经过修改后的值,所以很明显是实现一个经典的“区间修改+单点查询”的功能,可以借助树状数组这一数据结构完成;而实现0和1的翻转,我们可以通过判断操作数的奇偶来判断,当操作数为奇数时,那么对比起始一定进行了翻转操作,反之亦然。最后暴力遍历k的所有可能(因为最高才5000),维护最大值即可。
    PS:本题不能进行二分操作,因为答案不一定具备单调性!!!

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
#define INF 1e15
using namespace std;
int n,a[5010],b[5010];
char s[5010];
int diff[5010];
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
int T;cin>>T;
//树状数组实现“区间修改,单点查询”的功能
auto add1 = [&](int x, int k) -> void{
for (int i = x; i <= n;i+=i&(-i))
diff[i] += k;
};
auto add2 = [&](int l, int r, int x) -> void{
add1(l, x), add1(r + 1, -x);
};
auto query = [&](int x) -> int{
int res = 0;
for (int i = x; i;i-=i&(-i))
res += diff[i];
return res;
};
auto check = [&](int k) -> bool{
for (int i = 1; i <= n;i++)
b[i] = a[i];
for (int i = 1; i <= n; i++)
{
int cnt = query(i) % 2;
if (cnt) //操作次数为奇数时
b[i] ^= 1;
if (i + k - 1 <= n){
if (b[i] == 0)
add2(i, i + k - 1, 1);
}
else if (b[i] == 0)
return 0;
}
return 1;
};
while(T--){
cin >> n >> s + 1;
int ans;
for (int i = 1; i <= n; i++)
a[i] = s[i] - '0';
for (int i = 1; i <= n; i++){
if (check(i))
ans = i;
//记得每次操作过后要清除差分数组
for (int i = 1; i <= n;i++) diff[i] = 0;
}
cout << ans << endl;
}
return 0;
}

I. XOR on Segment(位运算+线段树)

官方评分:*2000

个人评分:*1700

本场最具价值的模板题,考察位运算的拆位贡献,且需要熟练运用线段树的模版.
需要线段树板子的,赛后记得找我要.

前置知识:线段树、异或

题意

有一个长度为n的数组a,可以对数组执行两个操作:

  • 1 l r:查询区间[l,r]的总和
  • 2 l r x:对区间[l,r]每个数与x异或,即

对原数组执行m次操作,输出每次查询的结果.

数据范围

思路

    由题意得,修改操作是位运算,所以我们拆开对每个位单独考虑;又因为a[i]的范围不超过1e6,所以只需要进行20位的讨论即可,我们通过二维的结构体建立约20课线段树,tree[i][x]表示从右往左第i位的线段树数组.

    对于异或操作,我们只需要对输入的x进行拆位,如果第i位为1,则区间内每个数的第i位都要进行取反操作,反之则没有任何影响;所以这题就演变成了线段树区间取反的模板题!其中,结构体变量ans表示该位上区间二进制值的和,size表示区间长度,lazy则表示该区间是否需要取反操作.

    对于区间查询总和的操作,因为线段树维护的信息是一个位上的区间”1”和”0”的总和,所以需要将每一位查询到的区间二进制和乘以位的权值,即.

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <bits/stdc++.h>
#define ll long long
#define endl '\n'
#define INF 1e15
using namespace std;
typedef pair<int, int> pii;
struct segment_tree{
int L, R ,size;
int lazy, ans;
} tree[21][400010];
int a[100010];
signed main(){
std::ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n;
for (int i = 1; i <= n;i++)
cin >> a[i];
auto up = [&](int x, int idx) -> void
{
tree[idx][x].ans = tree[idx][x << 1].ans + tree[idx][(x << 1) + 1].ans;
};
auto down = [&](int x, int idx) -> void
{
if (!tree[idx][x].lazy)
return;
tree[idx][x].ans = tree[idx][x].size - tree[idx][x].ans;
if (tree[idx][x].L!=tree[idx][x].R){
tree[idx][x << 1].lazy ^= 1;
tree[idx][(x << 1) + 1].lazy ^= 1;
}
tree[idx][x].lazy = 0;
};
auto build = [&](auto bulid, int x, int L, int R, int idx) -> void
{
tree[idx][x].L = L, tree[idx][x].R = R, tree[idx][x].size = R - L + 1;
if (L == R)
{
tree[idx][x].ans = ((a[L] >> idx) & 1);
return;
}
const int mid = L + R >> 1;
bulid(bulid, x << 1, L, mid, idx);
bulid(bulid, (x << 1) + 1, mid + 1, R, idx);
up(x, idx);
};
auto change = [&](auto change, int x, int L, int R, int idx) -> void
{
down(x, idx);
if (tree[idx][x].L > R || tree[idx][x].R < L)
return;
if (tree[idx][x].L >= L && tree[idx][x].R <= R)
{
tree[idx][x].lazy ^= 1;
down(x, idx);
return;
}
change(change, x << 1, L, R, idx);
change(change, (x << 1) + 1, L, R, idx);
up(x, idx);
};
auto query = [&](auto query, int x, int L, int R, int idx) -> int
{
down(x, idx);
if (tree[idx][x].L > R || tree[idx][x].R < L)
return 0;
if (tree[idx][x].L >= L && tree[idx][x].R <= R)
return tree[idx][x].ans;
return query(query, x << 1, L, R, idx) + query(query, (x << 1) + 1, L, R, idx);
};
for (int i = 0; i <= 20;i++)
build(build, 1, 1, n, i);
cin >> m;
while (m--){
int opt, L, R;
cin >> opt >> L >> R;
if (opt == 1){
ll sum = 0;
for (int i = 0; i <= 20;i++)
sum += (1LL << i) * query(query, 1, L, R, i);
cout << sum << endl;
}
else{
int x;
cin >> x;
for (int i = 0; i <= 20;i++)
if ((x >> i) & 1)
change(change, 1, L, R, i);
}
}
return 0;
}

J. Shuffle Party(模拟)

签到题,手玩一下样例就能轻松发现,当n不小于时,取最小值时的就是答案。其中,可以用(1 << x)位运算表示,不要使用pow函数,因为会有浮点数误差.

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
T = 1;cin >> T;
while (T--){
int n, cnt;
cin >> n;
for (int i = 0; i < 30; i++){
if (n >= (1 << i) && n < (1 << (i + 1))){
cnt = i;
break;
}
}
cout << (1 << cnt) << endl;
}
return 0;
}

K.Game with Multiset(贪心+位运算)

官方评分:*1300

个人评分:*1200

题意

最初有一个空的multiset容器(允许元素重复),有两种操作:

  1. ADD x——将一个元素加入集合
  2. GET w——判断当前多元素是否能够选择子集,使其和等于w

一共有m次操作,当进行操作2时应输出一个”YES”或”NO”.

数据范围

思路

    由于是位运算,我们依旧要进行拆位算贡献,显然,每个询问的w能否成立,我们只需要考虑其二进制为1的位置上是否能从集合中选择到至少一个元素(可以是合并后)。同时,因为,两个相同位i的值能合并成一个位为i+1的值。而对于元素的种类存在与否,只需要用进行计数存储。
    不难想到,我们从低位往高位进行枚举,如果集合中不存在当前位的元素,那么直接输出”NO”;反之,如果存在该元素,则将其数量减一,然后从贪心的角度考虑,为保证后方的位元素尽可能存在,我们将该位的数量除以二加到高位上,依次遍历和判断即可。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
int t, opt, x, w;
signed main(){
std::ios::sync_with_stdio(false);
cin >> t;
while (t--){
map<int, int> mp;
map<int, int> cur;
cin >> opt;
if (opt == 1)
cin >> x, mp[x]++;
else{
for (int i = 0; i < 30; i++)
cur[i] = mp[i];
cin >> w;
bool ok = 0;
for (int i = 0; i < 30; i++){ //从低位往高位枚举
cur[i] += cur[i - 1] / 2;
if (w & (1 << i)) //当第i位为'1'时
if (cur[i] > 0)
cur[i]--;
else{
ok = 1;
break;
}
}
cout << (ok ? "YES" : "NO")<<endl;
}
}
return 0;
}

L. Theofanis’ Nightmare(思维+贪心)

官方评分:*1400

个人评分:*1600

个人认为本场最具思维难度的题,该思路非常典,建议收藏

题意

给定一个长度为n的整数数组,现在将其分为若干段k(大小可变),使得分割后的值最大.

数据范围

思路

    因为本题的式子是段编号乘以区间和,况且是非定值的任意段数k,我们需要自己找好断点进行插入,既含有未知数k,又要涉及到编号和区间和,显然是难以下手的。
    比如[1][-6,2][5,6],该方案等于,将乘号转换为加号的角度看,也就是,不难发现,段编号乘以区间和的本质就是后缀和!因为每次执行后缀和加法的时候,越靠后的元素在总和的贡献次数越多,也就是影响了段编号。
    所以从贪心的角度考虑,显然,只要记录一个后缀和,当后缀和的值大于零,则立即进行分段,因为此时会对答案产生正贡献。

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
int t, n, a[100010], suf[100010];
signed main(){
std::ios::sync_with_stdio(false);
cin >> t;
while (t--)
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
int ans = 0;
for (int i = n; i >= 1; i--){
suf[i] = suf[i + 1] + a[i];
if (suf[i] > 0 || i == 1) //i=1必须分段,否则包含1的最前面一段可能直接舍弃了
ans += suf[i];
}
cout << ans << endl;
for (int i = 1; i <= n; i++)
suf[i] = 0;
}
return 0;
}

M. Money Trees(前缀和+二分)

官方评分:*1300

个人评分:*1200

本题最优解是O()做法的双指针,但是二分写法更加简洁易懂,虽然复杂度上多了个log

题意

    有n棵树,第i棵树有颗果实,高度为。现在选择连续子数组,对于每个i()都满足可以被整除。同时收集该区间的果实总数,保证果实总数不超过k。求子数组的最大长度.

数据范围

思路

    很明显,答案具有单调性,所以核心思想就是二分答案,判断条件就是“连续被整除”区间的果实总数小于等于k.
    所以问题转化为了判断区间是否为连续的”被整除”区间,对于每个数都判断能否被下个数整除,可以把该点的bool值赋为1(起始为0),如果该区间都能被连续整除,那么bool值的总和就是长度.
    不难想到,将这些bool值使用前缀和进行维护,就能以O(1)快速判断该区间是否为”连续被整除”区间,再套二分查找的板子,此题结.

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
int t, n, k, maxn, a[200010], s[200010], h[200010], len[200010];
signed main(){
std::ios::sync_with_stdio(false);cin.tie(0);
cin >> t;
auto check=[&](int x) -> bool{ //二分判断的bool函数
for (int i = 1; i <= n + 1 - x; i++)
if (s[i + x - 1] - s[i - 1] <= k && len[i + x - 1] - len[i] == x - 1)
return 1;
return 0;
};
while (t--){
cin >> n >> k;
bool flag = 0;
for (int i = 1; i <= n; i++){
cin >> a[i], s[i] = a[i] + s[i - 1];
if (a[i] <= k) flag = 1;
}
for (int i = 1; i <= n; i++){
cin >> h[i];
if (i == 1)
continue;
else if (h[i - 1] % h[i] == 0)
len[i] = len[i - 1] + 1;
else
len[i] = len[i - 1];
}
if (!flag){ //当所有数的果实数均超过k,那么直接特判最大长度为0
cout << 0 << endl;
continue;
}
int l = 1, r = 1e6, ans;
while (l <= r){
int mid = (l + r) >> 1;
if (check(mid))
l = mid + 1, ans = mid;
else
r = mid - 1;
}
cout << ans << endl;
}
return 0;
}