作为 (2020) 年的最后一篇博文,在今年 Codeforces 所有对积分 (ge 2100) 以上 Rated 的比赛中,挑选了有代表性的 (20) 道思维题在这里分享。
以下列举的题目为前 (10) 题((1) 月到 (6) 月),顺序为题目编号顺序。
1286C1 1286C2
题意
交互题,有一个长度为 (n),字符集为小写英文字母的字符串。
每次询问给定 (lle r),可以得到该字符串的子串 ([l,r]) 的所有子串,但这些子串是乱序给出的,每个子串里的字符也是乱序的。
需要用不超过 (3) 次询问得到这个字符串。
对于简单版,需要满足所有询问得到的子串个数之和不超过 ((n+1)^2)。
对于困难版,需要满足所有询问得到的子串个数之和不超过 (0.777(n+1)^2)。
(1le nle 100)。
Solution
可以发现这题的关键在于两个:
(1)把一个子串内的所有字符乱序给出相当于给出了这个子串内所有字符的出现次数;
(2)对于同一个询问得到的两个子串,只有按照它们的长度才能把它们区分开。
先考虑简单版,考虑 ([1,n]) 的所有长度为 (i) 的子串和 ([2,n]) 的所有长度为 (i) 的子串,易得 ([1,n]) 只比 ([2,n]) 多了一个子串 ([1,i])。
所以询问一次 ([1,n]) 一次 ([2,n]),对于每个 (1le ile n),求出 ([1,n]) 和 ([2,n]) 所有长度为 (i) 的子串各个字符出现次数的和,然后作差,就能得出 ([1,i]) 各个字符出现的次数,最后再对于每个 (i),让 ([1,i]) 与 ([1,i-1]) 作差即可得到 (i) 位置上的字符。
这样的询问次数为 (2),子串个数为 (inom{n+1}2+inom{n}2=n^2<(n+1)^2)。
对于困难版,考虑 ([1,n]) 还可以询问出什么:还是考虑所有长度为 (i) 的子串,不过这里尝试计算每个字符的贡献。
可以发现当 (ilelceilfrac n2 ceil) 时,原串一个位置 (j) 贡献了 (min(j,n-j+1,i)) 次,画成图大概是一个梯形,而长度变为 (i-1) 时 (j) 的贡献会变化当且仅当 (min(j,n-j+1)ge i),也就是 (jin[i,n-i+1])。
于是长度为 (i) 的所有子串之和减去长度为 (i-1) 的所有子串之和即为子串 ([i,n-i+1]),可以直观理解成两个梯形的面积相减。
然后再把这些形如 ([i,n-i+1]) 的子串进行差分,就能得到对于所有的 (1le ilelceilfrac n2 ceil),原串的第 (i) 位和第 (n-i+1) 位上的字符是什么(但不能得到哪个字符对应哪个位置,且特殊地如果 (n) 为奇数且 (i=lceilfrac n2 ceil) 则直接得到第 (i) 位的字符)。
于是这时候只需知道前一半(([1,lfloorfrac n2 floor]))的字符串就能知道后一半,对于前一半字符串跑一遍简单版即可。
这样询问数为 (3),子串个数不超过 (inom{n+1}2+(frac n2)^2<0.75(n+1)^2)。
Code
#include <bits/stdc++.h>
const int N = 105;
int n, m, sum[N][26], tot[N], cw[N][2], cur[26];
char s[N], ans[N];
int main()
{
std::cin >> n;
printf("? %d %d
", 1, n); fflush(stdout);
for (int i = 1; i <= n * (n + 1) / 2; i++)
{
scanf("%s", s + 1);
int len = strlen(s + 1);
for (int j = 1; j <= len; j++)
sum[len][s[j] - 'a']++;
}
m = n >> 1;
for (int i = 1; i <= m - ((n & 1) ^ 1); i++)
{
for (int c = 0; c < 26; c++) cur[c] = sum[i + 1][c];
for (int j = 1; j < i; j++) cur[cw[j][0]] += i - j + 1,
cur[cw[j][1]] += i - j + 1;
for (int c = 0; c < 26; c++) cur[c] = sum[1][c] * (i + 1) - cur[c];
for (int c = 0; c < 26; c++) while (cur[c]--) cw[i][tot[i]++] = c;
}
for (int c = 0; c < 26; c++) cur[c] = sum[1][c];
for (int i = 1; i <= m - ((n & 1) ^ 1); i++) cur[cw[i][0]]--, cur[cw[i][1]]--;
for (int c = 0; c < 26; c++) while (cur[c]--)
cw[m + (n & 1)][tot[m + (n & 1)]++] = c;
if (n & 1) ans[m + 1] = cw[m + 1][0] + 'a';
if (n == 1)
{
printf("! ");
for (int i = 1; i <= n; i++) putchar(s[1]);
return puts(""), 0;
}
printf("? %d %d
", 1, m); fflush(stdout);
memset(sum, 0, sizeof(sum));
for (int i = 1; i <= m * (m + 1) / 2; i++)
{
scanf("%s", s + 1);
int len = strlen(s + 1);
for (int j = 1; j <= len; j++)
sum[len][s[j] - 'a']++;
}
if (m > 1)
{
printf("? %d %d
", 2, m); fflush(stdout);
for (int i = 1; i <= m * (m - 1) / 2; i++)
{
scanf("%s", s + 1);
int len = strlen(s + 1);
for (int j = 1; j <= len; j++)
sum[len][s[j] - 'a']--;
}
}
for (int i = m; i >= 1; i--)
{
for (int c = 0; c < 26; c++) sum[i][c] -= sum[i - 1][c];
for (int c = 0; c < 26; c++) if (sum[i][c]) ans[i] = c + 'a';
ans[n - i + 1] = ans[i] == cw[i][0] + 'a' ? cw[i][1] + 'a'
: cw[i][0] + 'a';
}
printf("! ");
for (int i = 1; i <= n; i++) putchar(ans[i]);
return puts(""), 0;
}
1291F 1290D
Statement
交互题,有一个长度为 (n) 的序列 (a) 和一个长度为 (k) 的队列 (S),初始 (S) 为空,操作有两种:
(1)给定一个 (i),可以得到 (S) 里是否有和 (a_i) 相同的元素,并把 (a_i) 压入队列末尾,然后如果队列大小超过了 (k) 则弹队首;
(2)清空队列,可以使用不超过 (30000) 次。
(1le kle nle 1024) 且 (k) 和 (n) 都是 (2) 的整数次幂,保证 (frac{3n^2}{2k}le15000)。
对于简单版,要求操作(1)的次数不超过 (frac{2n^2}k);
对于困难版,要求操作(1)的次数不超过 (frac{3n^2}{2k})(实际存在不超过 (frac{n^2}k) 的做法)。
Solution
容易想到对于每个 (i),求出 (is_i) 是否不存在 (j<i) 使得 (a_j=a_i),这样答案就是 (sum is_i)。
考虑分块。块大小为 (max(frac k2,1)),一开始 (is) 全部为 (1)。
枚举两块 (j<i),尝试用第 (j) 块的元素去更新第 (i) 块的 (is),每次都要先清空队列。
依次把第 (j) 块和第 (i) 块内所有元素都加进去,期间如果加入一个元素时返回了 Yes 就将其 (is) 变成 (0),易得对于每个元素 (i),(i) 的左边与之同一块和不同块的元素都能与之进行检查,能保证正确性。
这样的总操作次数上限为 (n+inom{frac{2n}k}2frac k2=frac{2n^2}k),可过简单版。
如何优化?考虑如果我们依次要用第 (1) 块去更新第 (2) 块的 (is),第 (2) 块去更新第 (3) 块的 (is),…,那么这实际上不用对于所有 (frac nk-1) 次更新都用 (k) 次操作来实现,因为把前两块内的元素都加入之后,队列内还留着第 (2) 块内的元素,清空再加回来是没有必要的,可以继续用它来更新第 (3) 块的 (is)。有可能第 (3) 块会被第 (1) 块部分更新,但这不影响答案(只需保证每种数最终只剩第一个 (is=1))。
于是考虑一个 (frac {2n}k) 阶有向图,对于 (i<j) 由 (i) 向 (j) 连边,如果能把这个图的所有边组成若干条路径,那么对每条路径跑一遍上面的过程,由于对一条路径进行一遍过程的复杂度为 (点数 imes frac k2),所以可做到 ((inom{frac {2n}k}2+路径数) imesfrac k2) 的操作次数。
但是很遗憾,这个有向图看上去没有什么路径条数比较少的拆分方案,于是我们放弃 (is) 数组原来的定义,考虑一个新的做法(暴力):每次还是枚举 (i,j),但是用第 (i) 块去更新第 (j) 块还是用 (j) 去更新 (i) 是任意的。
可以证明如果所有的无序对 ((i,j)) 都被枚举到,那么每种数会且只会剩下一个 (is=1),正确性得到保证,不过这里要注意:如果用第 (j) 块更新第 (i) 块,那么必须只能把这两块内原来 (is=1) 的元素加入,否则可能某个数值的 (is) 会被全部变成 (0),正确性无法保证。
于是有向图就变成了无向图。但这里有另一个问题:对一条路径跑一遍过程的时候,对于路径上第 (i) 个点,该块内的元素即使在这个过程中 (is) 从 (1) 变成了 (0),还是会被加入这个队列,这样用第 (i) 个点更新第 (i+1) 个点时无法保证上面所说的正确性。
但如果这是一条简单路径(不经过重复的点),就可以说明这个正确性一定能保证。而如果这条路径走了一个环,且存在一个 (x) 使得环上每个点对应块内都有一个值 (x) 的 (is=1),那么走完一圈这些 (is) 都会变成 (0),即无法保证正确性。
于是我们必须保证这个 (frac{2n}k) 阶完全图拆出的每一条路径都是简单路径,随机化 DFS 可以得到比较优的方案,期望复杂度 (frac{1.2n^2}k),可以通过困难版(摘自原题解)。
但对于一个 (n)(偶数)个点的无向完全图,将其拆分成 (frac n2) 条经过所有点的链有一个经典构造方法:点从 (0) 开始标号,枚举 (0le i<frac n2),走这条路径:(i ightarrow i-1 ightarrow i+1 ightarrow i-2 ightarrow i+2 ightarrowdots)(其中每个点的编号对 (n) 取模),可以证明每条边都从某个方向被走了一遍。
总操作次数不超过 ((inom{frac{2n}k}2+frac nk) imesfrac k2=frac{n^2}k)。
此外,这题也可以设块大小为 (k),使用 (3k) 的操作次数用一块更新另一块,就不需要进行图论转化,具体可以看 jiangly 的代码。
Code
#include <bits/stdc++.h>
const int N = 1030;
bool query(int x)
{
char c;
printf("? %d
", x); fflush(stdout);
while ((c = getchar()) != 'N' && c != 'Y');
return c == 'Y';
}
int n, k, m, ans;
bool res[N];
int main()
{
std::cin >> n >> k;
if (k > 1) k /= 2; m = n / k;
memset(res, true, sizeof(res));
if (m == 1)
{
for (int i = 1; i <= n; i++) if (!query(i)) ans++;
return printf("! %d
", ans), 0;
}
for (int i = 0; i < m / 2; i++)
{
puts("R"); fflush(stdout);
for (int j = 0; j < m; j++)
{
int x = (i + (j & 1 ? -1 : 1) * (j + 1 >> 1) + m) % m;
for (int s = 1; s <= k; s++)
if (res[x * k + s] && query(x * k + s)) res[x * k + s] = 0;
}
}
for (int i = 1; i <= n; i++) ans += res[i];
return printf("! %d
", ans), 0;
}
1305F
题意
给定 (n) 个数 (a_{1dots n}),每次操作可以把一个数加 (1) 或减 (1),求让这 (n) 个数的 (gcd) 不是 (1) 的最小操作次数。注意你必须保证操作后每个数都为正。
(2le nle2 imes10^5),(1le a_ile10^{12})
Solution
人类智慧题。
Solution 1
由于让一个数成为偶数只需最多一次操作,所以答案小于 (n)。
考虑随机化:注意到由于答案小于 (n),所以至少有一半的数只操作了零次或一次。
可以每次随机一个数 (a) 钦定它操作不超过 (1) 次,找出 (a-1,a,a+1) 的所有质因子,然后对每个质因子 (p) 计算让所有数都是 (p) 的倍数的最小代价即可。注意代价一旦 (ge n) 就可以 break
掉。
随机 (20) 次,出错的概率小于 (2^{-20})。
复杂度上界 (O(k(sqrt a+nlog a))),(k=20),严重跑不满。
Solution 2
考虑枚举第一个数最后的值,在 ([a_1-n,a_1+n]) 范围内。
对 ([a_1-n,a_1+n]) 进行一遍区间埃筛,可以得到这个区间内所有数的质因子集合并。
然后对于每个质因子计算最小代价,还是一样,代价一旦 (ge n) 就可以 break
掉。
注意到对于质因子 (p),由于 break
期望情况下只需使用 (frac np) 的复杂度计算代价。
直观感受可以发现长得很像答案(计算代价的实际复杂度远大于 (frac np))的质因子个数是非常有限的,可以通过此题。
Code
Solution 1(by QAQAutoMaton)
/*
Author: QAQAutomaton
Lang: C++
Code: F.cpp
Mail: lk@qaq-am.com
Blog: https://www.qaq-am.com/
*/
#include<bits/stdc++.h>
#define int long long
#define debug(...) fprintf(stderr,__VA_ARGS__)
#define DEBUG printf("Passing [%s] in LINE %d
",__FUNCTION__,__LINE__)
#define Debug debug("Passing [%s] in LINE %d
",__FUNCTION__,__LINE__)
#define all(x) x.begin(),x.end()
#define x first
#define y second
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
int inf;
const double eps=1e-8;
const double pi=acos(-1.0);
template<class T,class T2>int chkmin(T &a,T2 b){return a>b?a=b,1:0;}
template<class T,class T2>int chkmax(T &a,T2 b){return a<b?a=b,1:0;}
template<class T>T sqr(T a){return a*a;}
template<class T,class T2>T mmin(T a,T2 b){return a<b?a:b;}
template<class T,class T2>T mmax(T a,T2 b){return a>b?a:b;}
template<class T>T aabs(T a){return a<0?-a:a;}
template<class T>int dcmp(T a,T b){return a>b;}
template<int *a>int cmp_a(int x,int y){return a[x]<a[y];}
#define min mmin
#define max mmax
#define abs aabs
struct __INIT__{
__INIT__(){
memset(&inf,0x3f,sizeof(inf));
}
}__INIT___;
namespace io {
const int SIZE = (1 << 21) + 1;
char ibuf[SIZE], *iS, *iT, obuf[SIZE], *oS = obuf, *oT = oS + SIZE - 1, c, qu[55]; int f, qr;
// getchar
#define gc() (iS == iT ? (iT = (iS = ibuf) + fread (ibuf, 1, SIZE, stdin), (iS == iT ? EOF : *iS ++)) : *iS ++)
// print the remaining part
inline void flush () {
fwrite (obuf, 1, oS - obuf, stdout);
oS = obuf;
}
// putchar
inline void putc (char x) {
*oS ++ = x;
if (oS == oT) flush ();
}
template<typename A>
inline bool read (A &x) {
for (f = 1, c = gc(); c < '0' || c > '9'; c = gc()) if (c == '-') f = -1;else if(c==EOF)return 0;
for (x = 0; c <= '9' && c >= '0'; c = gc()) x = x * 10 + (c & 15); x *= f;
return 1;
}
inline bool read (char &x) {
while((x=gc())==' '||x=='
' || x=='
');
return x!=EOF;
}
inline bool read(char *x){
while((*x=gc())=='
' || *x==' '||*x=='
');
if(*x==EOF)return 0;
while(!(*x=='
'||*x==' '||*x=='
'||*x==EOF))*(++x)=gc();
*x=0;
return 1;
}
template<typename A,typename ...B>
inline bool read(A &x,B &...y){
return read(x)&&read(y...);
}
template<typename A>
inline bool write (A x) {
if (!x) putc ('0'); if (x < 0) putc ('-'), x = -x;
while (x) qu[++ qr] = x % 10 + '0', x /= 10;
while (qr) putc (qu[qr --]);
return 0;
}
inline bool write (char x) {
putc(x);
return 0;
}
inline bool write(const char *x){
while(*x){putc(*x);++x;}
return 0;
}
inline bool write(char *x){
while(*x){putc(*x);++x;}
return 0;
}
template<typename A,typename ...B>
inline bool write(A x,B ...y){
return write(x)||write(y...);
}
//no need to call flush at the end manually!
struct Flusher_ {~Flusher_(){flush();}}io_flusher_;
}
using io :: read;
using io :: putc;
using io :: write;
int a[200005];
mt19937 ran;
vector<int> p;
int isp(int x){
for(int i=2;i*i<=x;++i)if(!(x%i))return 0;
return 1;
}
int cnt[20005];
void calc(int &x){
if(!x)return;
for(auto i:p){
if(!(x%i)){
++cnt[i];
while(!(x%i))x/=i;
}
}
}
int b[600005];
int gcd(int a,int b){return b?gcd(b,a%b):a;}
int n,xans;
void test(int x){
int ans=0;
for(int i=1;i<=n;++i){
ans+=min((a[i]%x)+(a[i]<x?x:0),x-a[i]%x);
if(ans>=xans)return;
}
xans=ans;
}
void yf(int &x,int y){
y=gcd(x,y);
if(y!=1)x=y;
}
void test2(int x){
int ans=0;
for(int i=1;i<=n;++i){
yf(x,a[i]);
yf(x,a[i]-1);
yf(x,a[i]+1);
int w=min(a[i]%x+(a[i]<x?x:0),x-a[i]%x);
ans+=w;
if(ans>=xans)return;
}
xans=ans;
}
signed main(){
#ifdef QAQAutoMaton
freopen("F.in","r",stdin);
freopen("F.out","w",stdout);
#endif
read(n);
xans=n;
ran=mt19937(time(0)^114514);
for(int i=2;i<=1000;++i)if(isp(i))p.push_back(i);
for(int i=1;i<=n;++i){
read(a[i]);
b[i*3-2]=a[i];
b[i*3-1]=a[i]-1;
b[i*3]=a[i]+1;
}
for(int i=1;i<=n+n+n;++i)calc(b[i]);
for(int i=1;i<=1000;++i)if(cnt[i]>=(n+1)>>1)test(i);
while(clock()<=2*CLOCKS_PER_SEC){
int x=ran()%(3*n)+1,y=ran()%(3*n)+1;
if(x!=y){
int w=gcd(b[x],b[y]);
if(w>1000){
test2(w);
}
}
}
write(xans,'
');
return 0;
}
Solution 2
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}
typedef long long ll;
const int N = 2e5 + 5, M = N << 1, T = 1234567;
int n, ans, m;
ll a[N], L, R, num[M];
bool mark[T];
void calc(ll num)
{
if (num == 1) return;
ll res = 0;
for (int i = 1; i <= n; i++)
if ((res += Min(a[i] < num ? num : a[i] % num,
(num - a[i] % num) % num)) >= n) return;
ans = Min(ans, (int) res);
}
int main()
{
read(n); ans = n;
for (int i = 1; i <= n; i++) read(a[i]);
L = a[1] - n; R = a[1] + n;
if (L < 1) L = 1; m = R - L + 1;
for (int i = 1; i <= m; i++) num[i] = L + i - 1;
for (int i = 2; i <= 2000; i++) if (!mark[i])
for (int j = i * i; j <= 1100000; j += i)
mark[j] = 1;
for (int x = 2; x <= 1100000; x++) if (!mark[x])
{
int st = L % x ? x - L % x + 1 : 1;
for (int j = st; j <= m; j += x)
while (num[j] % x == 0) num[j] /= x;
}
std::sort(num + 1, num + m + 1);
m = std::unique(num + 1, num + m + 1) - num - 1;
for (int x = 2; x <= 1100000; x++) if (!mark[x]) calc(x);
for (int i = 1; i <= m; i++) calc(num[i]);
return std::cout << ans << std::endl, 0;
}
1322D
题意
给定一个长度为 (n) 的数列 (l),选出一个不上升子序列,设这个子序列下标为 (a_1,a_2,dots,a_k)(即 (l_{a_1}ge l_{a_2}gedots l_{a_k}))。
假设有一个数为 (A),依次 (i) 从 (1) 到 (k),让 (A) 加上 (2^{l_{a_i}}),设 (cnt_i) 表示位权为 (2^i) 的二进制位值变化的次数,
求 (sum_icnt_ic_i-sum_{i=1}^ks_i) 的最大值。
(1le n,mle 2000),(1le l_ile m),(0le s_ile5000),(-5000le c_ile5000)。
Solution
为了方便,把 (l) 倒过来,选一个不下降子序列。
考虑 DP:设 (f_{i,j,k}) 表示已经考虑了 (l_1sim l_i) 中所有 (le j) 的 (l),往第 (j) 位之外进位了 (k) 次,就前 (j) 位的 (cnt imes c) 之和减去 (s) 之和的最大值。
转移分 (l_{i+1}) 选或不选进行讨论,还有一种是 (i) 不变,进行进位(让 (j) 加一),即算上第 (j+1) 位的贡献,从 (f_{i,j,k}) 转移到 (f_{i,j+1,lfloorfrac k2 floor})。
这个 DP 看上去是三方的,我们来分析一下复杂度,不难发现这个 (k) 不满。
具体地说,考虑加一个数 (2^i) 对所有位值变化次数的贡献,可以看出来依次第 (i,i+1,i+2,dots) 位的贡献分别是 (1,frac 12,frac 14,dots),越高的位对其变化次数的影响就越小,故对于所有的 (j),(k) 这一维的上界之和是 (O(n+m)) 的,故总复杂度 (O(n(n+m)))。
Code
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
template <class T>
inline T Max(const T &a, const T &b) {return a > b ? a : b;}
const int N = 2005, M = N << 1, INF = 0x3f3f3f3f;
int n, m, l[N], s[N], c[M], cnt[M], f[N][N], g[N][N];
bool vis[N];
std::vector<int> pt[M];
int main()
{
read(n); read(m); m += n;
for (int i = 1; i <= n; i++) read(l[i]), cnt[l[i]]++, pt[l[i]].push_back(i);
for (int i = 1; i <= n; i++) read(s[i]);
for (int i = 1; i <= m; i++) read(c[i]);
for (int i = 0; i < N; i++) for (int j = 0; j < N; j++)
f[i][j] = -INF;
for (int i = 1; i <= n; i++) f[i][0] = 0;
for (int i = 1; i <= m; i++)
{
memset(vis, 0, sizeof(vis));
for (int j = 0; j < pt[i].size(); j++) vis[pt[i][j]] = 1;
cnt[i] += cnt[i - 1];
for (int j = n; j >= 1; j--)
{
for (int k = cnt[i]; k >= 0; k--)
if (g[j][k] = k <= cnt[i - 1] ? f[j][k] : -INF, j < n)
g[j][k] = Max(g[j][k], g[j + 1][k]);
if (vis[j]) for (int k = cnt[i]; k >= 1; k--)
g[j][k] = Max(g[j][k], g[j][k - 1] - s[j]);
}
for (int j = 1; j <= n; j++)
{
for (int k = 0; k <= (cnt[i] >> 1); k++)
f[j][k] = -INF;
for (int k = 0; k <= cnt[i]; k++)
f[j][k >> 1] = Max(f[j][k >> 1], g[j][k] + k * c[i]);
}
cnt[i] >>= 1;
}
return std::cout << f[1][0] << std::endl, 0;
}
1326F1 1326F2
题意
给定一个 (n) 个点的有向图,对于一个 (1) 到 (n) 的排列 (p),生成一个长度为 (n-1) 的 (01) 串,第 (i) 个数表示 (p_i) 和 (p_{i+1}) 之间是否有边。
对于所有 (2^{n-1}) 种不同的 (01) 串,求出可以得到这个 (01) 串的排列 (p) 个数。
简单版:(2le nle 14)。
困难版:(2le nle 18)。
Solution
考虑 (01) 串中的 (0) 用容斥搞掉,即可以枚举集合 (S) 表示强制让下标集合 (S) 内的数为 (1),其他位置无限制的方案数,乘上容斥系数计入相应的答案。
故我们对于每个 (S) 求出强制下标集合 (S) 内的数为 (1),其他位置无限制的排列方案数,设为 (f_S),最后对 (f) 做一遍 IFMT
即可得到答案。
可以发现这相当于有若干个正整数 (a_1,a_2,dots,a_k) 满足 (sum_{i=1}^ka_i=n),依次选出 (k) 条点数分别为 (a_1,a_2,dots,a_k) 的路径,覆盖所有 (n) 个点的方案数。
我们可以发现对于任意的 (S),(n=18) 时本质不同的多重集 ({a_1,a_2,dots,a_k})(即 (n) 的正整数拆分)只有 (385) 种,而 (a) 数组的顺序对 (f) 值是没有影响的。
也就是说,本质不同的 (f) 只有 (385) 个,求出这 (385) 个 (f) 值即可。
而求单个 (f),设拆分为 (a_1,a_2,dots,a_k),(g_S) 为不重复地经过点集 (S) 内点的路径数,则这个 (f) 的值为 (sum_{s}prod_{i=1}^kg_{s_i}),需要满足 (|s_i|=a_i),所有 (s_i) 的并集为全集。
可以使用集合并卷积来做,即把 (g_S) 拓展成 (g_{|S|,S}),对 (g) 的每一行做一遍 FMT
预处理之后,求每个 (f) 时把所有对应的 (g_{a_i}) 乘起来。由于只需要求并集为全集的答案,故我们不需要做 (O(2^nn)) 的 IFMT
。
这样的复杂度是 (2^n) 乘上所有拆分方案的 (k) 之和,已经能通过此题。
不过还可以优化:考虑 DFS
枚举正整数拆分,枚举出一个 (a_i) 之后就立即把 (g_{a_i}) 乘上去,这样就避免了对每个拆分方案都枚举一遍 (a_{1dots k}),复杂度 (O(2^n(n^2+P(n)))),(P(n)) 为 (n) 的正整数拆分数。
Code(by jiangly)
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
int n;
std::vector<int> a;
std::vector<long long> ans;
std::map<std::vector<int>, long long> res;
std::vector<long long> chain;
std::vector<std::vector<long long>> chaint;
void dfs(int r, int x, std::vector<long long> g) {
if (r == 0) {
long long v = 0;
for (int i = 0; i < (1 << n); ++i)
v += __builtin_parity(((1 << n) - 1) ^ i) ? -g[i] : g[i];
res[a] = v;
return;
}
for (int i = x; i <= r; ++i) {
if (i != r && 2 * i > r)
continue;
a.push_back(i);
auto g0 = g;
for (int j = 0; j < (1 << n); ++j)
g0[j] *= chaint[i][j];
dfs(r - i, i, g0);
a.pop_back();
}
}
void f(std::vector<long long> &v) {
for (int i = 1; i < (1 << n); i *= 2)
for (int j = 0; j < (1 << n); j += 2 * i)
for (int k = 0; k < i; ++k)
v[i + j + k] += v[j + k];
}
void h(std::vector<long long> &v) {
for (int i = 1; i < (1 << n); i *= 2)
for (int j = 0; j < (1 << n); j += 2 * i)
for (int k = 0; k < i; ++k)
v[j + k] -= v[i + j + k];
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cin >> n;
std::vector<std::vector<bool>> g(n, std::vector<bool>(n));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
char c;
std::cin >> c;
g[i][j] = c - '0';
}
}
chaint.assign(1 << n, std::vector<long long>(n));
chain.resize(1 << n);
for (int s = 1; s < (1 << n); ++s) {
if ((s & -s) == s) {
chaint[s][__builtin_ctz(s)] = 1;
} else {
for (int v = 0; v < n; ++v)
if (s >> v & 1)
for (int u = 0; u < n; ++u)
if (g[u][v] && (s >> u & 1))
chaint[s][v] += chaint[s ^ 1 << v][u];
}
for (int i = 0; i < n; ++i)
chain[s] += chaint[s][i];
}
chaint.assign(n + 1, std::vector<long long>(1 << n));
for (int i = 1; i <= n; ++i) {
for (int s = 0; s < (1 << n); ++s)
if (__builtin_popcount(s) == i)
chaint[i][s] = chain[s];
f(chaint[i]);
}
dfs(n, 1, std::vector<long long>(1 << n, 1));
ans.resize(1 << n);
for (int s = 0; s < (1 << (n - 1)); ++s) {
std::vector<int> a;
int l = -1;
for (int i = 0; i < n - 1; ++i) {
if (~s >> i & 1) {
a.push_back(i - l);
l = i;
}
}
a.push_back(n - 1 - l);
std::sort(a.begin(), a.end());
ans[s] = res[a];
}
--n;
h(ans);
for (int i = 0; i < (1 << n); ++i)
std::cout << ans[i] << "
"[i == (1 << n) - 1];
return 0;
}
1329D
题意
给定一个字符串 (s),每次可以选择一个连续子串,满足这个子串中任意两个相邻的字符不同,把这个子串删掉,删掉之后该串剩下的两部分会连起来。
求最少多少次能删完这个串,并输出一种删除方案。
多测,数据组数不超过 (2 imes10^5),所有数据的 (|s|) 之和不超过 (2 imes10^5)。
Solution
如果原串不包含任意相邻相同的字符,则答案为 (1)。
否则考虑所有满足 (s_i=s_{i+1}) 的 (i)。可以发现一次操作最多减少 (2) 个这样的 (i)。
具体地,设 (s_i=s_{i+1}),(s_j=s_{j+1}),如果 (s_i=s_j) 则删子串 ([i+1,j]) 只会让相邻相同的字符对数减 (1),如果 (s_i e s_j) 则会减 (2)。
设满足 (s_i=s_{i+1}=c) 的 (i) 个数为 (cnt_c),则操作可以描述成找一对 (c e d),让 (cnt_c) 和 (cnt_d) 都减 (1),或者找单独的一个 (c) 让 (cnt_c) 减一,最后 (cnt) 变全 (0) 之后再全删掉。
这是一个经典问题(判断众数是否出现超过一半),可以得到答案为 (max(lceilfrac{sum c}2 ceil,max c)+1)。
注意到上面选出的 ((i,i+1)) 和 ((j,j+1)) 必须是相邻的(中间不能有相邻相同的字符对)。为了输出方案,考虑用 std::set
按下标顺序维护出所有的字符对,每次找一个出现次数最多的字符 (c),再找一个在 set
上与字符 (c) 出现位置有相邻的字符 (d),把这两个相邻的位置移除掉,并让 (cnt_c--,cnt_d--),直到最后只剩一种字符为止。
最后只剩一种字符时,就只能一次移除一个相邻相同的字符对了,依次移除即可。
这样我们就得到了一种方案,但我们得到的是每次操作的子串的首尾端点在哪个字符对上,而我们要输出的是每次操作的左右端点,故需要使用线段树或树状数组维护每个字符是否已经被删掉,把一个字符对对应到当前剩余的字符串的端点相当于在线段树或树状数组上查排名。
复杂度 (O(nlog n))。
Code(by wucstdio)
#include<cstdio>
#include<algorithm>
#include<cstring>
#define lc x<<1
#define rc x<<1|1
#define mid (l+r)/2
using namespace std;
struct Tree
{
int sum;
bool tag;
}tree[800005];
int T,n,m,pos[200005],cnt[26],u[200005],v[200005],num;
char s[200005],t[200005];
int st[200005],top;
void pushup(int x)
{
tree[x].sum=tree[lc].sum+tree[rc].sum;
}
void pushdown(int x,int l,int r)
{
if(tree[x].tag)
{
tree[lc].tag=tree[rc].tag=1;
tree[lc].sum=mid-l+1;
tree[rc].sum=r-mid;
tree[x].tag=0;
}
}
void build(int x,int l,int r)
{
tree[x].sum=tree[x].tag=0;
if(l==r)return;
build(lc,l,mid);
build(rc,mid+1,r);
}
void update(int x,int l,int r,int from,int to)
{
if(l>=from&&r<=to)
{
tree[x].tag=1;
tree[x].sum=r-l+1;
return;
}
pushdown(x,l,r);
if(from<=mid)update(lc,l,mid,from,to);
if(to>mid)update(rc,mid+1,r,from,to);
pushup(x);
}
int query(int x,int l,int r,int from,int to)
{
if(l>=from&&r<=to)return tree[x].sum;
pushdown(x,l,r);
int ans=0;
if(from<=mid)ans+=query(lc,l,mid,from,to);
if(to>mid)ans+=query(rc,mid+1,r,from,to);
return ans;
}
bool check()
{
int sum=0,maxx=0;
for(int i=0;i<26;i++)
{
sum+=cnt[i];
maxx=max(maxx,cnt[i]);
}
return maxx*2<=sum;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%s",s+1);
n=(int)strlen(s+1);
memset(cnt,0,sizeof(cnt));
m=0;
for(int i=2;i<=n;i++)
{
if(s[i]==s[i-1])
{
t[++m]=s[i];
cnt[s[i]-'a']++;
pos[m]=i;
}
}
t[++m]=' ';
m--;
int res=0;
for(int c=0;c<26;c++)
if(cnt[c]>cnt[res])res=c;
num=0;
if(cnt[res]*2<=m)
{
if(m&1)cnt[t[m]-'a']--,m--;
top=0;
for(int i=1;i<=m;i++)
{
if(!top||t[i]==t[st[top]])st[++top]=i;
else
{
cnt[t[i]-'a']--;
cnt[t[st[top]]-'a']--;
if(check())
{
num++;
u[num]=pos[st[top]],v[num]=pos[i]-1;
top--;
}
else cnt[t[i]-'a']++,cnt[t[st[top]]-'a']++,st[++top]=i;
}
}
}
else
{
top=0;
for(int i=1;i<=m;i++)
{
if(t[i]=='a'+res)
{
if(t[st[top]]=='a'+res||!top)st[++top]=i;
else
{
num++;
u[num]=pos[st[top]],v[num]=pos[i]-1;
top--;
}
}
else
{
if(t[st[top]]=='a'+res&&top)
{
num++;
u[num]=pos[st[top]],v[num]=pos[i]-1;
top--;
}
else st[++top]=i;
}
}
}
build(1,1,n);
for(int i=1;i<=num;i++)
{
int x=u[i]-query(1,1,n,1,u[i]);
int y=v[i]-query(1,1,n,1,v[i]);
update(1,1,n,u[i],v[i]);
u[i]=x,v[i]=y;
}
m=0;
for(int i=1;i<=n;i++)
{
if(query(1,1,n,i,i))continue;
t[++m]=s[i];
}
int last=1;
for(int i=2;i<=m;i++)
{
if(t[i]==t[i-1])
{
u[++num]=1;
v[num]=i-last;
last=i;
}
}
u[++num]=1;
v[num]=m+1-last;
printf("%d
",num);
for(int i=1;i<=num;i++)printf("%d %d
",u[i],v[i]);
}
return 0;
}
1338D
题意
给定一棵树,点数为 (n)。你需要画出 (n) 个封闭图形,满足对于任意的 (i e j),第 (i) 个图形与第 (j) 个图形有交(这里的有交定义为边界有公共点)当且仅当树上有边 ((i,j))。
求最多能选出多少个图形,使得这些图形满足大的图形嵌套小的图形。
(3le nle10^5)。
Solution
显然最大的图形不能被全面包围(和外界不相通)。考虑选一个没有被包围(和外界相通)的点 (u) 为根,考虑 (u) 的所有出边。
删掉点 (u) 代表的图形之后,剩下的 (n-1) 个图形被分成了若干个连通区域,每个连通区域代表一个子树。
对于边 ((u,v)),如果 (u) 出现在了嵌套子集中,(v) 就显然不能出现在子集中,但 (v) 的子树可以出现。
而如果让 (u) 作为嵌套子集的最大图形,则只能选择一个子节点 (v) 进行继承,因为所有子树的占有区域不交。
再考虑 (v) 的所有子节点 (w),显然我们要把 (w) 的子树都安排在图形 (u) 的里面。同样地我们可以选择一个 (w) 进行继承。
但这里我们能注意到:这里 (v) 所有的子节点都选择了 (v) 边上的一段弧进行相交。如果参与继承的子树为 (w),则对于 (v) 的一个不是 (w) 的子节点 (x),我们其实是可以让 (x) 套在 (w) 的外面的。
也就是说,在这个地方多了 (v) 的度数减 (2) 的贡献。
以此类推,得出一个结论:
答案即为选一条链,将链上部分点染黑,相邻点不能同时为黑,白点的贡献为其度数减 (2),黑点的贡献为 (1)。
直接换根树 DP 即可,(O(n))。
Code
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
template <class T>
inline T Max(const T &a, const T &b) {return a > b ? a : b;}
const int N = 1e5 + 5, M = N << 1;
int n, ecnt, nxt[M], adj[N], go[M], f[N][2], ans, d[N];
void add_edge(int u, int v)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u;
d[u]++; d[v]++;
}
void dfs(int u, int fu)
{
bool is = 1; int f1 = -1, f2 = -1, f3 = -1, f4 = -1;
for (int e = adj[u], v; e; e = nxt[e])
if ((v = go[e]) != fu)
{
dfs(v, u); is = 0;
f[u][0] = Max(f[u][0], Max(f[v][0], f[v][1]));
f[u][1] = Max(f[u][1], f[v][0]);
if (f[v][0] > f1) f2 = f1, f1 = f[v][0];
else if (f[v][0] > f2) f2 = f[v][0];
if (Max(f[v][0], f[v][1]) > f3) f4 = f3, f3 = Max(f[v][0], f[v][1]);
else if (Max(f[v][0], f[v][1]) > f4) f4 = Max(f[v][0], f[v][1]);
}
if (is) f[u][1] = 1;
else f[u][0] += d[u] - 2, f[u][1]++, ans = Max(ans, f[u][0] + 1);
ans = Max(ans, f[u][1]);
if (f1 != -1 && f2 != -1) ans = Max(ans, Max(f1 + f2 + 1,
f3 + f4 + d[u] - 2));
}
int main()
{
int x, y;
read(n);
for (int i = 1; i < n; i++) read(x), read(y), add_edge(x, y);
dfs(1, 0);
return std::cout << ans << std::endl, 0;
}
1336D
题意
交互题,你有一堆麻将,点数从 (1) 到 (n),每种点数的麻将个数在 ([0,n]) 之间,但你不知道它们具体是多少。
初始时可以知道这堆麻将中,碰(大小为 (3) 且点数相同的子集)的个数和吃(大小为 (3) 且点数形成公差为 (1) 的等差数列)的个数。
然后你可以加入最多 (n) 次某一种点数的麻将,加入一个麻将之后你可以得到此时碰和吃的个数,你需要还原初始时每种点数的麻将个数。
数据范围;(4le nle 100)。
Solution
设当前第 (i) 种麻将有 (c_i) 个,则加入一个第 (i) 种麻将时会多出 (inom{c_i}2) 个碰和 (c_{i-2}c_{i-1}+c_{i-1}c_{i+1}+c_{i+1}c_{i+2}) 个吃。
如果不考虑吃的个数,则如果保证 (c_i>0) 则可以通过碰的个数的增量还原出 (c_i)。
考虑求点数为 (1) 的个数,可以得到如果事先加入一个 (1),就能保证 (c_i>0),再加入一个 (1) 即可查出 (ans_1),而加入 (1) 的好处是吃的个数增量为 (c_2c_3)。
于是考虑依次加入 (3,1,2,1),这样第二次吃的个数增量为 (ans_2(ans_3+1)),第四次吃的个数增量为 ((ans_2+1)(ans_3+1)),这两个式子作差即可得到 (ans_3)。由于 (ans_3+1>0),故可以使用除法得到 (ans_2)。
而实际上我们也可以得到 (ans_4):考虑第三次吃的个数增量:((ans_3+1)(ans_1+1+ans_4)),也可以利用除法得到。
而对于 (i>4),也可以加入一个 (i-2),这时吃的个数增量表达式中只有 (ans_i) 是未知量,可以解出来。不过这样有一个问题:(ans_{i-1}) 可能为 (0),这样的方程会有无穷多个解。
故考虑倒着加:(n-1,n-2,dots,3,1,2,1),易得 (3,1,2,1) 移到最后不影响 (ans_{1dots 4}) 的求解,只是 (n>4) 时这样求解出来的 (ans_4) 需要减 (1)(在 (n-1,n-2,dots 4) 中加上了 (1))。
然后 (i) 从 (3) 到 (n-2),利用 (i) 被加入时吃的个数增量来解出 (ans_{i+2}),由于 (i+1) 在之前的过程中加过了 (1),故可以保证 (c_{i+1}) 不为 (0),这个方程一定可以解出来。
(O(n)),操作次数为 (n)。
Code
#include <bits/stdc++.h>
const int N = 110, M = N * N;
int n, ans[N], f[M], a[N], b[N];
void add(int v) {printf("+ %d
", v); fflush(stdout);}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n + 1; i++) f[i * (i - 1) >> 1] = i;
scanf("%*d%*d");
for (int i = 1; i <= n - 4; i++) add(n - i), scanf("%d%d", &a[i], &b[i]);
add(3); scanf("%d%d", &a[n - 3], &b[n - 3]);
add(1); scanf("%d%d", &a[n - 2], &b[n - 2]);
add(2); scanf("%d%d", &a[n - 1], &b[n - 1]);
add(1); scanf("%d%d", &a[n], &b[n]);
ans[1] = f[a[n] - a[n - 1]] - 1;
ans[3] = (b[n] - b[n - 1]) - (b[n - 2] - b[n - 3]) - 1;
ans[2] = (b[n] - b[n - 1]) / (ans[3] + 1) - 1;
ans[4] = (b[n - 1] - b[n - 2]) / (ans[3] + 1) - (ans[1] + 1) - (n > 4);
for (int i = n - 3; i >= 2; i--)
{
int x = n - i;
ans[x + 2] = (b[i] - b[i - 1] - ans[x - 2] * ans[x - 1] - ans[x - 1]
* (ans[x + 1] + 1)) / (ans[x + 1] + 1) - (i > 2);
}
printf("! ");
for (int i = 1; i <= n; i++) printf("%d ", ans[i]);
return puts(""), 0;
}
1361D
题意
平面上给定 (n) 个互不相同的点,其中一个点是原点。
建一棵树,原点为根,一个不为原点的点的父亲为其到原点的线段上的第二个点,边长即为到父亲的欧几里得距离。
求选出 (k) 个不同的点,这些点两两距离和最小值。
(2le kle nle 5 imes10^5)。
Solution
这种树的生成方式只会导致树的一个特殊性质:根的所有子树都为链。
性质:对于根的任意子树,若选出了 (c) 个点,则最优方案下子树内深度最大的 (min(lfloorfrac k2 floor,c)) 个点都必须选出。
证明:
考虑把一个点的位置往子树内移动 (1) 个长度单位时答案会怎么变化。
设子树内已经选出了 (s) 个点,则该点和这 (s) 个点的距离都会 (-1),到其他 (k-1-s) 个点的距离都会 (+1),贡献为 (k-1-2s)。
故当 (s<lfloorfrac k2 floor) 时,这个贡献一定为正。
有了这个性质之后,就考虑每条边的贡献(被经过的次数乘长度),即一条边长为 (l),子树内有 (s) 个关键点的边,贡献为 (l imes s imes(k-s))。
假设每个子树内的点数都不超过 (lfloorfrac k2 floor),则按深度从大到小加点,若这是该子树内第 (i) 次加点,其到根的距离为 (d),则这个点到根上所有边的 (s(k-s)) 都会发生变化,也就是贡献为:
显然当 (i<lfloorfrac k2 floor) 时,其父亲的贡献比自己小,故把贡献排序之后选最大的 (k) 个即可,然后考虑如果有子树内的点数超过 (lfloorfrac k2 floor) 怎么处理,显然这样的子树最多一个。
根据上面的贡献式,可以得出在一个已经选出 (lfloorfrac k2 floor) 个点的子树内再选点是负贡献,故只有在前面排序后不够选出贡献最大的 (k) 个时,才采用这种策略。同时由于选根的贡献为 (0),故这时根一定要选上。
故设前面已经选出了 (tot) 个正贡献的点,再选了一个根之后,还需要选出 (k-tot-1) 个点。与前面性质的证明类似,可以得出这 (k-tot-1) 个点必然是某个子树中深度最浅的 (k-tot-1) 个,枚举这个子树计算答案即可。
总复杂度 (O(nlog n))。
Code
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
typedef long long ll;
const int N = 5e5 + 5;
int n, k, m, l[N], r[N], bel[N], tot;
double ans;
bool vis[N];
struct djq
{
int u; double dis;
} w[N];
inline bool pmoc(djq a, djq b) {return a.dis > b.dis;}
struct point
{
int x, y;
friend inline ll operator * (point a, point b)
{
return 1ll * a.x * b.y - 1ll * a.y * b.x;
}
ll len() {return 1ll * x * x + 1ll * y * y;}
} a[N];
inline bool comp(point a, point b)
{
bool isa = a.y > 0 || (a.y == 0 && a.x > 0),
isb = b.y > 0 || (b.y == 0 && b.x > 0);
if (isa != isb) return isa > isb;
return a * b > 0 || (a * b == 0 && a.len() < b.len());
}
bool coll(point a, point b)
{
bool isa = a.y > 0 || (a.y == 0 && a.x > 0),
isb = b.y > 0 || (b.y == 0 && b.x > 0);
return isa == isb && a * b == 0;
}
int main()
{
read(n); read(k);
for (int i = 1; i <= n; i++) read(a[i].x), read(a[i].y);
for (int i = 1; i <= n; i++) if (!a[i].x && !a[i].y)
{std::swap(a[i], a[1]); break;}
std::sort(a + 2, a + n + 1, comp);
for (int i = 2, j = 2; i <= n; i++)
if (i == n || !coll(a[i], a[i + 1]))
{
l[++m] = j; r[m] = i;
for (int k = j; k <= i; k++) bel[k] = m;
for (int h = 1; h <= k / 2 && h <= i - j + 1; h++)
w[++tot] = (djq) {i - h + 1, sqrt(a[i - h + 1].len())
* (k + 1 - 2 * h)};
j = i + 1;
}
std::sort(w + 1, w + tot + 1, pmoc);
for (int i = 1; i <= tot && i <= k; i++) ans += w[i].dis, vis[w[i].u] = 1;
if (tot < k)
{
double delta = -1e24;
for (int T = 1; T <= m; T++)
{
int cnt = 0;
for (int i = l[T]; i <= r[T]; i++) if (!vis[i]) cnt++;
if (cnt < k - tot - 1) continue;
double res = 0;
for (int i = 1; i < k - tot; i++)
res += sqrt(a[l[T] + (k - tot - 1) - i].len())
* (k + 1 - 2 * (k / 2 + i));
if (res > delta) delta = res;
}
ans += delta;
}
return printf("%.10lf
", ans), 0;
}
1361E
题意
给定一个 (n) 个点 (m) 条边的强连通有向图,定义一个点 (u) 是好的当且仅当 (u) 到所有的节点存在唯一简单路径。
求出所有好点,特殊地,如果好点的个数严格小于 (frac n5) 则输出 (-1)。
(1le nle 10^5),(0le mle 2 imes10^5),多测,所有数据的 (n) 之和不超过 (10^5),所有数据的 (m) 之和不超过 (2 imes10^5)。
Solution
这是一道巧妙的图论题。
先考虑一个点 (u) 是好的条件:以 (u) 为根的 DFS 树唯一。判断是否唯一只需以 (u) 为根后跑出任意一棵 DFS 树,判断是否所有的非树边都是后代指向祖先即可,证明比较显然。
考虑随便找一个点判断它是否是好的,随机 (100) 次,如果找不到,就可以认为好的点数严格小于 (frac n5),出错的概率只有 (2 imes10^{-10})。
这样就找到了任意一个好点 (r),先跑一棵生成树,考虑一个点 (u e r) 是好的条件:
性质 (1):若有超过一条边从 (u) 的子树内指向子树外,则 (u) 不是好的。
证明:
若以 (u) 为根,则原树上 (u) 子树外的点必然是 (u) 子树内的点的子孙;
而如果这样的边有两条,则只有一条能成为树边,另一条必然违反了 (u) 为好点的条件。
由于所有的树边都是后代指向祖先,故 (u) 从子树内指向子树外的边可以简单预处理,同时,由于原图强连通,故对于每个 (u),这样的返祖边一定存在。
性质 (2):在性质 (1) 的前提下,设这条返祖边指向的点为 (v),则 (u) 是好的当且仅当 (v) 是好的。
证明:
充分性:如果 (v) 是好的,则考虑以 (v) 为根的 DFS 树,原树上 (u) 的子树在以 (v) 为根的 DFS 树上必然还是一棵子树,于是把 (u) 的父亲到 (u) 的边切断之后,(v) 仍然是好的。由于保证了就 (u) 的子树来说 (u) 是好的,又因为 (u) 的子树只有一条树边连向外面,故这样得到的生成树唯一,得证。
必要性:如果 (u) 是好的,则这棵唯一的生成树一定是 (u) 的子树加上这条返祖边再加上子树外的部分,于是就 (u) 子树外的部分来说,(v) 是好的,同时 (u) 的子树内外只有返祖边,故 (v) 是好的,得证。
于是从上往下推即可,复杂度 (O(T(n+m))),其中 (T) 为随机次数。
Code
#include <bits/stdc++.h>
template <class T>
inline void read(T &res)
{
res = 0; bool bo = 0; char c;
while (((c = getchar()) < '0' || c > '9') && c != '-');
if (c == '-') bo = 1; else res = c - 48;
while ((c = getchar()) >= '0' && c <= '9')
res = (res << 3) + (res << 1) + (c - 48);
if (bo) res = ~res + 1;
}
const int N = 1e5 + 5, M = N << 1;
int n, m, ecnt, nxt[M], adj[N], st[M], go[M], tot, ow[M], ToT, dfn[N],
sze[N], seq[N], dep[N], f[N], cnt[N];
bool vis[N], ans[N], siv[M];
void add_edge(int u, int v)
{
nxt[++ecnt] = adj[u]; adj[u] = ecnt; st[ecnt] = u; go[ecnt] = v;
}
void dfs(int u)
{
vis[u] = 1; seq[dfn[u] = ++ToT] = u; sze[u] = 1;
for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
if (!vis[v]) dep[v] = dep[u] + 1, dfs(v), sze[u] += sze[v], siv[e] = 1;
else ow[++tot] = e;
}
void work()
{
int x, y, T = 100;
read(n); read(m); ecnt = 0;
for (int i = 1; i <= n; i++) adj[i] = 0;
while (m--) read(x), read(y), add_edge(x, y);
while (T--)
{
for (int i = 1; i <= n; i++) vis[i] = 0, cnt[i] = 0;
for (int i = 1; i <= ecnt; i++) siv[i] = 0;
int rt = (rand() << 15 | rand()) % n + 1, res = 1;
tot = ToT = 0; dep[rt] = 1; dfs(rt); bool is = 1;
for (int i = 1; i <= tot; i++)
{
int e = ow[i]; cnt[st[e]]++; cnt[go[e]]--;
if (dfn[go[e]] > dfn[st[e]] || dfn[st[e]] >= dfn[go[e]] + sze[go[e]])
{is = 0; break;}
}
if (!is) continue;
for (int i = 1; i <= n; i++) f[i] = 0;
ans[rt] = 1;
for (int i = n; i >= 1; i--)
{
int u = seq[i];
for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
if (siv[e])
{
cnt[u] += cnt[v];
if (f[v] && (!f[u] || dep[f[v]] < dep[f[u]]))
f[u] = f[v];
}
else if (!f[u] || dep[go[e]] < dep[f[u]]) f[u] = go[e];
}
for (int i = 2; i <= n; i++)
res += (ans[seq[i]] = cnt[seq[i]] == 1 && ans[f[seq[i]]]);
if (res >= (n + 4) / 5)
{
for (int i = 1; i <= n; i++) if (ans[i]) printf("%d ", i);
puts("");
}
else puts("-1");
return;
}
puts("-1");
}
int main()
{
int T; read(T);
while (T--) work();
return 0;
}