E. Left Shifting 3
分析:签到模拟,破环成链。
cpp
#include <bits/stdc++.h>
using namespace std;
void solve()
{
int n, k;
cin >> n >> k;
vector<char> s(n * 2 + 1);
for (int i = 0; i < n; i++)
cin >> s[i];
k = min(n, k);
for (int i = n; i <= n + k - 1; i++)
{
s[i] = s[i - n];
}
vector<int> a(n * 2 + 1, 0), snum(n * 2 + 1, 0);
for (int i = 0; i <= n + k - 1 - 5; i++)
{
if (s[i] == 'n' && s[i + 1] == 'a' && s[i + 2] == 'n' && s[i + 3] == 'j' && s[i + 4] == 'i' && s[i + 5] == 'n' && s[i + 6] == 'g')
{
a[i + 6] = 1;
}
}
for (int i = 1; i <= n + k; i ++){
snum[i] = snum[i-1] + a[i-1];
}
int mx = 0;
for (int i = 1; i + n - 1 <= n + k; i++)
{
int j = i + n - 1;
mx = max(mx, snum[j] - snum[i - 1]);
}
cout << mx << endl;
}
int main()
{
int t;
cin >> t;
while (t--)
solve();
return 0;
}
J. Social Media
分析:分类讨论。
case1 : 找到一个和当前点有连接的两个点 可以直接计算复杂度等于边数*2
case2 : 或者找两个不相连但是和联通块的连接很大的 这个时候直接找两个就可以复杂度等于点数
时间复杂度是:O(NlogN)+O(MlogN)+O(MlogM)+O(K)
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
void solve()
{
int n, m, k;
cin >> n >> m >> k;
vector<vector<int>> adj(k + 1);
vector<int> value(k + 1); // 记录了如果选这个人可以创造的价值(就是这个人和原来几个人的互评数)
set<int> st;
for (int i = 0; i < n; i++)
{
int x;
cin >> x;
st.insert(x); // 标记最初的几个人是谁 不能选这几个人
}
int ans = 0;
for (int i = 0; i < m; i++)
{
// 边的个数
int u, v;
cin >> u >> v;
if(u != v){
adj[u].push_back(v);
adj[v].push_back(u);
}
if(st.count(u) && st.count(v))
ans++;
else if(st.count(u) && !st.count(v)){
value[v] ++ ;
}else if(!st.count(u) && st.count(v)){
value[u]++;
}else{
if( u == v) // 注意考虑自己给自己评论创造的价值
value[u]++;
}
}
// 枚举可能选择的点
int tmp = ans;
for(int i = 1; i <= k ; i ++){
if(!st.count(i)){
int t_ans = tmp;
// case1 : 找到一个和当前点有连接的两个点 可以直接计算复杂度等于边数*2
map<int , int>mp;
for(auto v : adj[i])
mp[v]++;
for (auto p : mp)
if(!st.count(p.first)){
ans = max(ans, t_ans + p.second + value[p.first] + value[i]);
}
}
}
// case2 : 或者找两个不相连但是和联通块的连接很大的 这个时候直接找两个就可以复杂度等于点数
int mx1 = 0, mx2 = 0, d = 0;
for (int i = 1; i<= k ; i ++){
if(!st.count(i)){
if(value[i] > mx1){
mx2 = mx1 ;
mx1 = value[i];
}else if(value[i] > mx2){
mx2 = value[i];
}
}
}
ans = max(tmp + mx1 + mx2, ans);
// 看最多的两个有没有发消息 或者看发了多少消息
cout << ans << endl;
}
int main()
{
int t;
cin >> t;
while (t--)
solve();
return 0;
}
K. Strips
分析:反悔贪心。
我们尽可能的对于遍历到的每一个需要覆盖的点,都从这个点开始覆盖,这样可以尽可能少用条带,如果发现覆盖之后会覆盖不该覆盖的就把这个条带左移,我们已经记录了前面的条带的起始和终止的位置。
Case一:如果当前的条带和上一个条带中间是有不能覆盖的格子的。
则当前的条带最多能移动到这些不能覆盖的格子中的最右侧的格子的右侧,看是否能满足需要移动的间隔,如果能满足则移动并break去处理下一个需要覆盖的格子,不能满足则-1。
Case一:如果当前的条带和上一个条带中间是没有不能覆盖的格子。
case1:如果和前面的条带有足够的间隔去移动就直接移动即可,并break去处理下一个需要覆盖的格子。
case2:如果没有足够的间隔去移动就把这两个相邻的条带合并成一个大的条带再去移动,为了方便要记录此时可以移动的间隔,直到sum(可以移动的间隔)>= 需要移动的间隔break,重复上面的过程。
复杂度就是是 **O(n log n + m log m + n log m)**的。
cpp
#include<bits/stdc++.h>
using namespace std;
#define x first
#define y second
#define PII pair<int ,int>
vector<PII> v;
void solve(){
int n , m , k , w;
cin >> n >> m >> k >> w;
vector<int>a(n) , b(m + 2);
for (int i = 0; i < n; i++)
cin >> a[i];
for (int i = 1; i <= m; i++){
cin >> b[i];
}
b[0] = 0, b[m + 1] = w + 1; // 易错点1
sort(a.begin(), a.end());
sort(b.begin(), b.end());
vector<PII> seg; // 存已经相连的覆盖的线段
seg.push_back({0, 0});
for (int i = 0; i < n; i++){
// 看最后相连的线段满不满足要求
if (seg.back().y >= a[i])
continue;
//不可以的话就需要开一个线段
seg.push_back({a[i], a[i] + k - 1});
// 看右边第一个临界的状态是哪个
int posr = lower_bound(b.begin(), b.end(), a[i]) - b.begin();
int rbr = b[posr];
if (seg.back().y < rbr) // 没有产生影响 否则的话就需要左移
continue;
int nd_move = seg.back().y - rbr + 1; // 需要移动的间隔
while(1){
// 看和前一个段中间的空隙
int posl = lower_bound(b.begin(), b.end(), seg[seg.size() - 2].y) - b.begin(); //找到临界的rbl的位置
int rbl = b[posl];
if(rbl == rbr){
// 说明是一个东西就不用但心
int ok_move = seg.back().x - seg[seg.size() - 2].y - 1; // 最多可以移动的间隔
if(ok_move >= nd_move){
// 可以移动的话就直接移动然后break
seg.back().y -= nd_move;
seg.back().x -= nd_move;
break;
}else{
//可以合并成一个大的段继续判断 不可以直接达成目标就尽可能先和前一个段合并
//再去看这个合成的段和前一个段能不能达成目标
if(seg.size() >= 3){
// 易错点2 因为如果剩余段是3 还是可以合并最后两段 再去判断一次
// 当剩余段是2的时候 就需要直接退出了 因为合并成一段之后无法继续
nd_move -= ok_move;
int cur_len = seg.back().y - seg.back().x + 1; //长度
seg.pop_back();
seg.back().y += cur_len;
}else{
// 段数<=2合并不了就退出
cout << -1 << endl;
return;
}
}
}else{
// 和前一个段被控制的不是同一个黑色的块 依旧可以尝试移动到最最左边的黑块的右边
int black_r = lower_bound(b.begin() , b.end() , seg.back().x) - b.begin() - 1;
int ok_move = seg.back().x - b[black_r] - 1;
if( ok_move >= nd_move){
//代表可以移动
seg.back().x -= nd_move;
seg.back().y -= nd_move;
break;
}else{
cout << -1 <<endl;
return;
}
}
}
}
// 然后找到所有可以移动的段
int len = 0;
vector<PII> ans;
for (auto p : seg)
{
if(p.y ==0 && p.x == 0 ) // 易错点3
continue;
len += (p.y - p.x + 1);
ans.push_back({p.x, (p.y - p.x + 1) / k}); // 起点和数量
}
cout << len / k << endl; // 肯定是k的倍数
for (auto p : ans)
{
for (int i = 0; i < p.y; i++)
{
cout << p.x + i * k << " ";
}
}
cout << endl;
}
int main(){
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t;
cin >> t;
while(t -- )
solve();
return 0;
}
B. Birthday Gift
分析:这个题就是纯纯的思维题了,知道结论或者方法的话非常简答,不知道的话就很牢。
我们发现奇数位置的两个0是无论如何是不能抵消的,所以抵消的一定是奇数和偶数位置的相同数,所以我们去记录偶数位置的0 1 2的数量,以及奇数位置的 0 1 2 数量,注意偶数位置把0统计成1 , 把1统计成0(可以想象偶数位置都翻转,只有统计过后的数字不相同才可以抵消) , 这样最后不考虑2得到的结果就是max(cnt1 , cnt0) - min(cnt1 , cnt0) ,无论如何最后的结果一定全是1或者全是0,这个时候把2考虑进去,尽可能转换成小的,可以把上面的式子尽可能小所以最后的答案就是:ans = max(cnt1 , cnt0) - min(cnt1 , cnt0) - cnt2。
cpp
#include <bits/stdc++.h>
using namespace std;
#define endl "\n"
#define ll long long
void solve()
{
string s;
cin >> s;
int n = s.size();
int cnt0 = 0, cnt1 = 0, cnt2 = 0;
// 统计偶数位置翻转后的情况 2 不用管
for(int i = 0 ; i< s.size() ; i ++){
if(i % 2 == 1){
// 这就是偶数位置
if(s[i] == '0') cnt1 ++;
else if(s[i] == '1') cnt0 ++;
else
cnt2++;
}else{
if (s[i] == '0')
cnt0++;
else if (s[i] == '1')
cnt1++;
else
cnt2++;
}
}
int ans = max(cnt1 , cnt0) - min(cnt1 , cnt0) - cnt2;
if(ans < 0 ){
if(abs(ans) % 2)
cout << 1 << endl;
else
cout << 0 << endl;
}else{
cout << ans << endl;
}
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
ll t;
cin >> t;
while (t--)
solve();
return 0;
}
G. Binary Tree
分析:收获颇多的一个交互题,要理解树的重心。
题目让我们找到一个隐藏点,每次可以询问两个点到这个隐藏点谁的距离更近,让我们不超过log(n)的次数,我们如果学习过树的重心就可以知道,树的重心的最大的子树的大小是<=总的节点数量/2的,如果我们每次询问都从树的重心出发会不会每次把需要考虑的点数减少一半呢?
寻找树重心的模板代码,要非常熟悉。
cpp
vector<int> mss(n + 1), siz(n + 1);
auto dfs = [&](auto &&dfs, int u, int fa) -> void //dfs出所有每个点作为根的树的大小 以及 每个点向下的子树的最大子树
{
siz[u] = 1;
for(auto v : adj[u]){
if(v == fa)
continue;
dfs(dfs , v , u);
siz[u] += siz[v];
mss[u] = max(mss[u], siz[v]);
}
};
dfs(dfs, s, -1);
int tot = siz[s] , rt;
for (int i = 1; i <= n; i++){
mss[i] = max(mss[i], tot - siz[i]); // tot-siz[i] 是父亲节点方向的子树大小
if(siz[i] && mss[i] <= tot/ 2){ // 注意不能选择被剪掉的 所以要保证siz[i]>0
rt = i; //这就是重心
}
}
假设我们已经找到了树的重心是rt。
case1:如果此时和rt相连的边数是0,那就理所当然的直接输出rt了。
case2:如果和rt相连的边数是1,那么我们询问rt和这个相连的点。
if(询问结果是0)就代表rt离隐藏点更近,显然直接输出rt即可。
else 代表隐藏点在这个相连的点或它的子树中
case3:如果和rt相连的边数是2,那么我们就直接询问这两个点,为什么不询问rt和其中的一个点呢,答案是效率较低,我们直接询问这两个点,(可以思考一下询问根是不是数据规模不一定小1/2)
if(1) 就代表隐藏点就是rt,if(2) or if(0) 就代表是其中一个点或它的子树中,点数的规模一定会减少1/2以上的。
case4:如果和rt相连的边数是3,其实这种情况和上一种情况是类似的,我们想要找到数据规模缩小一半的询问,就会发现询问根是做不到的,如果询问和根相连的最大的两个子树,就恰好可以满足我们的要求。
if(1)就代表隐藏点是rt或是那个最小的子树,if(2) or if(0)隐藏点就分别一定在最大的两个子树和与rt相连的两个点上,数据规模一定会缩小一半以上,太神奇拉~
根据询问的结果去修剪不需要考虑的子树。
cpp
if(d == 0 ){
s = u ;
adj[s].erase(find(adj[s].begin(), adj[s].end(), rt));
}else if(d == 2){
s = v;
adj[s].erase(find(adj[s].begin(), adj[s].end(), rt));
}else{ //
s = rt;
adj[s].erase(find(adj[s].begin(), adj[s].end(), u));
adj[s].erase(find(adj[s].begin(), adj[s].end(), v));
}
然后重复上述过程,最多不超过log2(n)次。
cpp
#include <bits/stdc++.h>
using namespace std;
#define ll long long
void solve()
{
int n;
cin >> n;
vector<vector<int>> adj(n + 1);
for(int i = 1 ; i <= n ; i ++){
int u, v;
cin >> u >> v;
if( u != 0 )
adj[i].push_back(u), adj[u].push_back(i);
if(v != 0)
adj[i].push_back(v), adj[v].push_back(i);
}
auto ask = [&](int u, int v)->int{
cout << "? " << u << " " << v << endl;
int x;
cin >> x;
return x;
};
auto answer = [&](int x) -> void
{
cout << "! " << x << endl;
};
int s = 1;
while(1){
vector<int> mss(n + 1), siz(n + 1);
auto dfs = [&](auto &&dfs, int u, int fa) -> void //dfs出所有每个点作为根的树的大小 以及 每个点向下的子树的最大子树
{
siz[u] = 1;
for(auto v : adj[u]){
if(v == fa)
continue;
dfs(dfs , v , u);
siz[u] += siz[v];
mss[u] = max(mss[u], siz[v]);
}
};
dfs(dfs, s, -1);
int tot = siz[s] , rt;
for (int i = 1; i <= n; i++){
mss[i] = max(mss[i], tot - siz[i]); // tot-siz[i] 是父亲节点方向的子树大小
if(siz[i] && mss[i] <= tot/ 2){ // 注意不能选择被剪掉的 所以要保证siz[i]>0
rt = i; //这就是重心
}
}
if(adj[rt].size() == 0){
answer(rt);
return;
}else if(adj[rt].size() == 1){
int v = adj[rt][0];
int d = ask(rt, v);
if(d == 0){
answer(rt);
return;
}else{
answer(v);
return;
}
}else
{ // adj[rt].size() == 2 和 adj[rt].size() == 3 的处理方法是相通的 所以可以只写一遍
sort(adj[rt].begin(), adj[rt].end(), [&](auto lx, auto ly)
{ return siz[lx] > siz[ly]; });
int u = adj[rt][0] , v =adj[rt][1];
int d = ask(u, v);
if(d == 0 ){
s = u ;
adj[s].erase(find(adj[s].begin(), adj[s].end(), rt));
}else if(d == 2){
s = v;
adj[s].erase(find(adj[s].begin(), adj[s].end(), rt));
}else{ //
s = rt;
adj[s].erase(find(adj[s].begin(), adj[s].end(), u));
adj[s].erase(find(adj[s].begin(), adj[s].end(), v));
}
}
}
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
int t;
cin >> t;
while (t--)
solve();
return 0;
}