这次被B和C题炸成稀碎了,4min秒了A然后坐牢1h,后面懒得想了干脆睡觉去了。
开始补题,首先回顾一下A。
首先我们知道Kalamaki是在每个偶数回合行动,则它可以操作的下标为 和 ,并看一下要不要交换它们。则我们显然可以发现,如果 ,Kalamaki可以做到让这两个数降序排列,从而赢得整个游戏,因为Souvlaki无法改变这个已经交换的结果。
我们可以发现为了赢,Souvlaki只能将整个序列升序排序,因为如果非升序,则Kalamaki都不需要操作就可以赢得游戏。所以我们先将整个序列排序,然后判断此时是否存在一个 ,使得 ,从而Kalamaki可以将它们交换从而赢得游戏。
#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;
#define ll long long
#define ull unsigned long long
#define debug(x) cout << #x << "=" << x << "\n";
int t;
int n;
const int maxn = 110;
int a[maxn];
void solve()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
sort(a + 1, a + n + 1);
for (int i = 2; i < n; i += 2)
{
if (a[i] != a[i + 1])
return void(cout << "NO\n");
}
cout << "YES\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> t;
while (t--)
solve();
return 0;
}
总体时间复杂度: ,没什么好说的。
然后就来到了让我深恶痛绝的B题,评价如下:
不是我说白了你那构造是人能想出来的吗...
来看看构造思路,我们先看一下什么情况下无解。如果 ,且 或 ,则显然无法找到一个区间 ,使得 或 ,此时直接输出 ;另外,如果 且 或 ,则向左向右都找不到多余的数来包含它,此时也直接输出 。
接下来我们来证明对于剩下的所有情况,都可以找到一种通用可行的策略来使得条件满足。
我们考虑两个位置 ,有 ,则我们通过四个端点 来构造对应的区间。
首先我们选取区间 ,则对于所有的 和 ,都可以使得 ,同理,我们选取区间 ,对于所有的 和 ,都可以使得 ,然后再选取区间 ,则这个区间内的所有数 ,都有 ,则我们的构造就完成了。
反思一下,以后碰到排列和有关范围问题,可以考虑使用这个排列内最小的 和最大的 来构造求解问题,同时合理运用交集和并集关系。
#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <vector>
#include <algorithm>
using namespace std;
#define ll long long
#define ull unsigned long long
#define debug(x) cout << #x << "=" << x << "\n";
int t;
int n;
const int maxn = 2e5 + 10;
int p[maxn];
string x;
int pre_min[maxn], pre_max[maxn];
int back_min[maxn], back_max[maxn];
void solve()
{
cin >> n;
int pos_1, pos_n;
for (int i = 1; i <= n; i++)
cin >> p[i], pos_1 = (p[i] == 1 ? i : pos_1), pos_n = (p[i] == n ? i : pos_n);
cin >> x;
if (find(x.begin(), x.end(), '1') == x.end())
return void(cout << "0\n");
if (x[0] == '1' || x[n - 1] == '1')
return void(cout << "-1\n");
for (int i = 0; i < n; i++)
{
if (x[i] == '1' && (p[i + 1] == 1 || p[i + 1] == n))
return void(cout << "-1\n");
}
cout << "5\n";
cout << 1 << " " << pos_1 << "\n";
cout << 1 << " " << pos_n << "\n";
cout << pos_1 << " " << n << "\n";
cout << pos_n << " " << n << "\n";
cout << min(pos_1, pos_n) << " " << max(pos_1, pos_n) << "\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> t;
while (t--)
solve();
return 0;
}
总体时间复杂度: ,直接扫一遍序列。
C题,这位更是重量级,不过感觉比B题好一点。
我们先固定 ,看看此时 有什么性质。假设区间 是“好的”,由于 ,则我们可以推得 也是好的。其实也很明显,当你扩大范围的时候, 中出现 的个数只会更多而不会减少,所以对于原来存在的路径在扩大范围后依然存在。
则我们可以设 为使 为“好的”的最小的 ,此时再减小 就会使路径被截断。则 都是可以和 配对的选择,所以此时满足条件的 的个数为 。
而且我们又可以看出来,若有 ,则 ,这个可以根据集合的大小关系推出来:若有 ,则又 ,故 也为满足条件的 ,可以与 配对,但这与 的最小性矛盾,故 。
OK此时我们可以看出来随着 的增大, 是只增不减的,具有单调性,所以我们很容易就想到可以使用双指针来维护这个 移动的过程。
但是又有一个难点就是我们应该怎么判断对于每个 的连通性呢?题目提到了一个很关键的性质:我们只能向下或者向右走,而此时又根据只有两行的这个特殊性质,所以我们至多只会向下走一次。我们设第一行第一个“截断点”即出现 的位置为 ,第二行出现的最后一个“截断点”的位置为 ,则我们如果要保证连通,就要使 ,即存在一列 ,使得 均为 且分别和第一行的开头以及第二行的末尾连通。
具体的我们可以怎么维护呢?考虑到我们需要维护的是此时第一行值为 的列和第二行值为 的列的集合,又需要根据 的移动来频繁插入和删除,且这个需要插入和删除的列的序号可能是不连续的,所以我们使用STL中的 容器来维护这个过程。
具体的,由于 ,所以我们可以根据 的值来存储对应的下标,同时使用两个 来分别维护第一行和第二行未被加入的列的序号。因此当我们移动 时,可以根据 的值来确定对应的坐标 有哪些,然后进行插入或删除,同时判断当前这个 是否满足条件,若满足条件则进行计数,并将 向右移动。
#include <iostream>
#include <cstring>
#include <iomanip>
#include <cmath>
#include <vector>
#include <algorithm>
#include <set>
#include <climits>
using namespace std;
#define ll long long
#define ull unsigned long long
#define debug(x) cout << #x << "=" << x << "\n";
int t;
int n;
const int maxn = 2e5 + 10;
const int INF = INT_MAX;
int a[3][maxn];
void solve()
{
cin >> n;
vector<vector<pair<int, int>>> pos(2 * n + 1);
for (int i = 1; i <= 2; i++)
{
for (int j = 1; j <= n; j++)
{
cin >> a[i][j];
pos[a[i][j]].push_back({i, j});
}
}
vector<set<int>> st(3);
st[1].insert(INF); // 防止空集时取begin和rbegin导致错误,减少空集判断的代码,更加简洁
st[2].insert(-INF);
for (int i = 1; i <= n; i++)
st[1].insert(i), st[2].insert(i);
auto add = [&](int val)
{
for (auto [i, j] : pos[val])
st[i].erase(j);
};
auto del = [&](int val)
{
for (auto [i, j] : pos[val])
st[i].insert(j);
};
auto check = [&]()
{
if (st[1].count(1) || st[2].count(n))
return false;
if (*st[1].begin() - 1 <= *st[2].rbegin())
return false;
return true;
};
ll ans = 0;
ll r = 0;
for (ll l = 1; l <= 2 * n; l++)
{
while (r + 1 <= 2 * n && !check())
add(++r);
if (!check()) // 如果此时还是不满足条件,则后面的l都不可能满足条件了,直接退出循环
break;
ans += 2 * n - r + 1;
del(l);
}
cout << ans << "\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> t;
while (t--)
solve();
return 0;
}
整体时间复杂度: ,主要在移动 和 的插入、删除上。
好了后面的题目就不补了,超出能力范围了。
大概总结一下,这次可以看出我的构造能力还是太差,唉唉,然后STL用得也不够熟,至少C题这种使用方式是我之前基本从来没有用过的。应当反省,分析问题时要找出问题的特殊性,然后从特殊性入手来求解。