传送门
题面
思路
这道题太 fake 了,打个暴力都能过,丢分居然是因为没有判断重边和自环 QAQ,唉,我太弱啦!
显然我们可以把这些人分成一段一段的,然后分治处理,则最后的答案就是分治处理过程形成的树的最大深度 - 1。所以现在的关键是如何找到分割点。
什么都不会的我当然是选择暴力啦!暴力枚举分割点,然后暴力检查:判断后面部分是否正确向前面部分连了边,且后面部分的每个点向前面部分连边的数量恰好为 。可以通过对边排序后二分查找实现。
int divide(int l, int r)
{
if (l == r)
return 0;
for (int i = l, to = l + ((r - l + 1) >> 1) - 1; i <= to; i++)
{
bool bOk = true;
for (int j = i + 1, t = l; j <= r; j++, t = (t == i ? l : t + 1))
{
if (!(std::binary_search(G[j].begin(), G[j].end(), t) &&
countRange(G[j], l, i) == 1))
{
bOk = false;
break;
}
}
if (bOk)
{
int a = divide(l, i);
int b = divide(i + 1, r);
if (a == -1 || b == -1)
return -1;
return 1 + std::max(a, b);
}
}
return -1;
}
然后我就得到了 分!!!当然我就不会再管这道题了,最后下来告诉我,那十分是因为存在重边和自环。出题人十分良心因此只在前面的两个点里加了重边和自环。唉,我太弱了,写了一个复杂度错误特判错误下标错误的程序,没想到居然得到了 分,唉,我太弱啦!
正解
正解只提到了如何找分割点。怎么判断是否合法?不可能,这辈子都不会遇到写得详细的题解了,毕竟大家都是神仙,唉,我太弱啦!
现在考虑如何找到分割点。对于区间 ,考虑 连向的在范围内的编号最小的那个点,设为 ,那么显然这就是后半部分连向前半部分的一条边。那么显然, 一定连向了 ,而 自然就连向了前半部分编号最大的点了,这样我们就找到分割点了。特别地,若 连向的点使得 分成了相同大小的两块,那么 连向的这个点就是分割点。
然后怎么办呢?像暴力那样强行判断是否合法就可以了。
参考代码
(C++ 11)
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <cassert>
#include <cctype>
#include <climits>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <map>
#include <set>
#include <bitset>
#include <list>
#include <functional>
typedef long long LL;
typedef unsigned long long ULL;
using std::cin;
using std::cout;
using std::endl;
typedef int INT_PUT;
INT_PUT readIn()
{
INT_PUT a = 0; bool positive = true;
char ch = getchar();
while (!(ch == '-' || std::isdigit(ch))) ch = getchar();
if (ch == '-') { positive = false; ch = getchar(); }
while (std::isdigit(ch)) { a = a * 10 - (ch - '0'); ch = getchar(); }
return positive ? -a : a;
}
void printOut(INT_PUT x)
{
char buffer[20]; int length = 0;
if (x < 0) putchar('-'); else x = -x;
do buffer[length++] = -(x % 10) + '0'; while (x /= 10);
do putchar(buffer[--length]); while (length);
putchar('\n');
}
const int maxn = int(1e5) + 5;
int n, m;
std::vector<std::vector<int> > G;
int countRange(const std::vector<int>& G, int l, int r)
{
return std::upper_bound(G.begin(), G.end(), r) - std::lower_bound(G.begin(), G.end(), l);
}
bool check(int l, int r, int mid)
{
for (int i = mid + 1, t = l; i <= r; i++, t = (t == mid ? l : t + 1))
{
if (!(std::binary_search(G[i].begin(), G[i].end(), t) &&
countRange(G[i], l, mid) == 1))
{
return false;
}
}
return true;
}
int divide(int l, int r)
{
if (l == r)
return 0;
auto it = std::lower_bound(G[r].begin(), G[r].end(), l);
if (it == G[r].end() || *it > r)
return -1;
int pre = r - (*it - l + 1);
if (pre < l)
return -1;
if (pre - l + 1 == r - pre)
{
if (!check(l, r, pre))
return -1;
int a = divide(l, pre);
int b = divide(pre + 1, r);
if (a == -1 || b == -1)
return -1;
return 1 + std::max(a, b);
}
it = std::lower_bound(G[pre].begin(), G[pre].end(), l);
if (it == G[pre].end() || *it > r || *it - l + 1 > r - *it)
return -1;
if (!check(l, r, *it))
return -1;
int a = divide(l, *it);
int b = divide(*it + 1, r);
if (a == -1 || b == -1)
return -1;
return 1 + std::max(a, b);
}
void run()
{
int T = readIn();
while (T--)
{
G.clear();
n = readIn();
m = readIn();
G.resize(n);
bool bOk = true;
for (int i = 1; i <= m; i++)
{
int from = readIn();
int to = readIn();
if (from == to)
{
bOk = false;
}
if (from < to) std::swap(from, to);
G[from].push_back(to);
}
if (!bOk)
{
printOut(-1);
continue;
}
for (int i = 0; i < n; i++)
std::sort(G[i].begin(), G[i].end());
for (int i = 0; i < n; i++)
{
int size = G[i].size();
int newsize = std::unique(G[i].begin(), G[i].end()) - G[i].begin();
if (size != newsize)
{
bOk = false;
break;
}
}
if (!bOk)
{
printOut(-1);
continue;
}
printOut(divide(0, n - 1));
}
}
int main()
{
#ifndef LOCAL
freopen("gymnastics.in", "r", stdin);
freopen("gymnastics.out", "w", stdout);
#endif
run();
return 0;
}
时间复杂度
寻找分割点的时间复杂度显然是 的,这一步的总时间复杂度显然是 的。现在我们考虑判断的时间复杂度。
考虑最坏情况,当然是分割点在很前面,需要花费 的时间去判断后面部分的点与前面部分的连接情况。即使在最好情况也需要花费 的时间。难道这道题是道玄学题?
当然不(shì)。由于判断一个点是否满足要求相当于要检查一条边,而边数是 的,且边不会被重复检查,因此时间复杂度是 的。