Codeforces 思維題彙總 2020(上篇)
作為 \(2020\) 年的最後一篇博文,在今年 Codeforces 所有對積分 \(\ge 2100\) 以上 Rated 的比賽中,挑選了有代表性的 \(20\) 道思維題在這裡分享。
以下列舉的題目為前 \(10\) 題(\(1\) 月到 \(6\) 月),順序為題目編號順序。
1286C1 1286C2
題意
互動題,有一個長度為 \(n\),字符集為小寫英文字母的字串。
每次詢問給定 \(l\le r\),可以得到該字串的子串 \([l,r]\) 的所有子串,但這些子串是亂序給出的,每個子串裡的字元也是亂序的。
需要用不超過 \(3\) 次詢問得到這個字串。
對於簡單版,需要滿足所有詢問得到的子串個數之和不超過 \((n+1)^2\)
對於困難版,需要滿足所有詢問得到的子串個數之和不超過 \(0.777(n+1)^2\)。
\(1\le n\le 100\)。
Solution
可以發現這題的關鍵在於兩個:
(1)把一個子串內的所有字元亂序給出相當於給出了這個子串內所有字元的出現次數;
(2)對於同一個詢問得到的兩個子串,只有按照它們的長度才能把它們區分開。
先考慮簡單版,考慮 \([1,n]\) 的所有長度為 \(i\) 的子串和 \([2,n]\) 的所有長度為 \(i\) 的子串,易得 \([1,n]\) 只比 \([2,n]\) 多了一個子串 \([1,i]\)。
所以詢問一次 \([1,n]\) 一次 \([2,n]\)
這樣的詢問次數為 \(2\),子串個數為 \(\binom{n+1}2+\binom{n}2=n^2<(n+1)^2\)。
對於困難版,考慮 \([1,n]\) 還可以詢問出什麼:還是考慮所有長度為 \(i\) 的子串,不過這裡嘗試計算每個字元的貢獻。
可以發現當 \(i\le\lceil\frac n2\rceil\)
於是長度為 \(i\) 的所有子串之和減去長度為 \(i-1\) 的所有子串之和即為子串 \([i,n-i+1]\),可以直觀理解成兩個梯形的面積相減。
然後再把這些形如 \([i,n-i+1]\) 的子串進行差分,就能得到對於所有的 \(1\le i\le\lceil\frac n2\rceil\),原串的第 \(i\) 位和第 \(n-i+1\) 位上的字元是什麼(但不能得到哪個字元對應哪個位置,且特殊地如果 \(n\) 為奇數且 \(i=\lceil\frac n2\rceil\) 則直接得到第 \(i\) 位的字元)。
於是這時候只需知道前一半(\([1,\lfloor\frac n2\rfloor]\))的字串就能知道後一半,對於前一半字串跑一遍簡單版即可。
這樣詢問數為 \(3\),子串個數不超過 \(\binom{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\n", 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\n", 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\n", 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\) 次。
\(1\le k\le n\le 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+\binom{\frac{2n}k}2\frac 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\) 連邊,如果能把這個圖的所有邊組成若干條路徑,那麼對每條路徑跑一遍上面的過程,由於對一條路徑進行一遍過程的複雜度為 \(點數\times \frac k2\),所以可做到 \((\binom{\frac {2n}k}2+路徑數)\times\frac 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\) 開始標號,列舉 \(0\le i<\frac n2\),走這條路徑:\(i\rightarrow i-1\rightarrow i+1\rightarrow i-2\rightarrow i+2\rightarrow\dots\)(其中每個點的編號對 \(n\) 取模),可以證明每條邊都從某個方向被走了一遍。
總操作次數不超過 \((\binom{\frac{2n}k}2+\frac nk)\times\frac 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\n", 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\n", 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\n", ans), 0;
}
1305F
題意
給定 \(n\) 個數 \(a_{1\dots n}\),每次操作可以把一個數加 \(1\) 或減 \(1\),求讓這 \(n\) 個數的 \(\gcd\) 不是 \(1\) 的最小操作次數。注意你必須保證操作後每個數都為正。
\(2\le n\le2\times10^5\),\(1\le a_i\le10^{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+n\log 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: [email protected]
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\n",__FUNCTION__,__LINE__)
#define Debug debug("Passing [%s] in LINE %d\n",__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=='\n' || x=='\r');
return x!=EOF;
}
inline bool read(char *x){
while((*x=gc())=='\n' || *x==' '||*x=='\r');
if(*x==EOF)return 0;
while(!(*x=='\n'||*x==' '||*x=='\r'||*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,'\n');
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}\ge\dots 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\) 的最大值。
\(1\le n,m\le 2000\),\(1\le l_i\le m\),\(0\le s_i\le5000\),\(-5000\le c_i\le5000\)。
Solution
為了方便,把 \(l\) 倒過來,選一個不下降子序列。
考慮 DP:設 \(f_{i,j,k}\) 表示已經考慮了 \(l_1\sim l_i\) 中所有 \(\le j\) 的 \(l\),往第 \(j\) 位之外進位了 \(k\) 次,就前 \(j\) 位的 \(cnt\times c\) 之和減去 \(s\) 之和的最大值。
轉移分 \(l_{i+1}\) 選或不選進行討論,還有一種是 \(i\) 不變,進行進位(讓 \(j\) 加一),即算上第 \(j+1\) 位的貢獻,從 \(f_{i,j,k}\) 轉移到 \(f_{i,j+1,\lfloor\frac k2\rfloor}\)。
這個 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\) 個數。
簡單版:\(2\le n\le 14\)。
困難版:\(2\le n\le 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_{1\dots 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] << " \n"[i == (1 << n) - 1];
return 0;
}
1329D
題意
給定一個字串 \(s\),每次可以選擇一個連續子串,滿足這個子串中任意兩個相鄰的字元不同,把這個子串刪掉,刪掉之後該串剩下的兩部分會連起來。
求最少多少次能刪完這個串,並輸出一種刪除方案。
多測,資料組數不超過 \(2\times10^5\),所有資料的 \(|s|\) 之和不超過 \(2\times10^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\ne s_j\) 則會減 \(2\)。
設滿足 \(s_i=s_{i+1}=c\) 的 \(i\) 個數為 \(cnt_c\),則操作可以描述成找一對 \(c\ne d\),讓 \(cnt_c\) 和 \(cnt_d\) 都減 \(1\),或者找單獨的一個 \(c\) 讓 \(cnt_c\) 減一,最後 \(cnt\) 變全 \(0\) 之後再全刪掉。
這是一個經典問題(判斷眾數是否出現超過一半),可以得到答案為 \(\max(\lceil\frac{\sum c}2\rceil,\max c)+1\)。
注意到上面選出的 \((i,i+1)\) 和 \((j,j+1)\) 必須是相鄰的(中間不能有相鄰相同的字元對)。為了輸出方案,考慮用 std::set
按下標順序維護出所有的字元對,每次找一個出現次數最多的字元 \(c\),再找一個在 set
上與字元 \(c\) 出現位置有相鄰的字元 \(d\),把這兩個相鄰的位置移除掉,並讓 \(cnt_c--,cnt_d--\),直到最後只剩一種字元為止。
最後只剩一種字元時,就只能一次移除一個相鄰相同的字元對了,依次移除即可。
這樣我們就得到了一種方案,但我們得到的是每次操作的子串的首尾端點在哪個字元對上,而我們要輸出的是每次操作的左右端點,故需要使用線段樹或樹狀陣列維護每個字元是否已經被刪掉,把一個字元對對應到當前剩餘的字串的端點相當於線上段樹或樹狀陣列上查排名。
複雜度 \(O(n\log 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]='\0';
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\n",num);
for(int i=1;i<=num;i++)printf("%d %d\n",u[i],v[i]);
}
return 0;
}
1338D
題意
給定一棵樹,點數為 \(n\)。你需要畫出 \(n\) 個封閉圖形,滿足對於任意的 \(i\ne j\),第 \(i\) 個圖形與第 \(j\) 個圖形有交(這裡的有交定義為邊界有公共點)當且僅當樹上有邊 \((i,j)\)。
求最多能選出多少個圖形,使得這些圖形滿足大的圖形巢狀小的圖形。
\(3\le n\le10^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\) 次某一種點數的麻將,加入一個麻將之後你可以得到此時碰和吃的個數,你需要還原初始時每種點數的麻將個數。
資料範圍;\(4\le n\le 100\)。
Solution
設當前第 \(i\) 種麻將有 \(c_i\) 個,則加入一個第 \(i\) 種麻將時會多出 \(\binom{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_{1\dots 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\n", 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\) 個不同的點,這些點兩兩距離和最小值。
\(2\le k\le n\le 5\times10^5\)。
Solution
這種樹的生成方式只會導致樹的一個特殊性質:根的所有子樹都為鏈。
性質:對於根的任意子樹,若選出了 \(c\) 個點,則最優方案下子樹內深度最大的 \(\min(\lfloor\frac k2\rfloor,c)\) 個點都必須選出。
證明:
考慮把一個點的位置往子樹內移動 \(1\) 個長度單位時答案會怎麼變化。
設子樹內已經選出了 \(s\) 個點,則該點和這 \(s\) 個點的距離都會 \(-1\),到其他 \(k-1-s\) 個點的距離都會 \(+1\),貢獻為 \(k-1-2s\)。
故當 \(s<\lfloor\frac k2\rfloor\) 時,這個貢獻一定為正。
有了這個性質之後,就考慮每條邊的貢獻(被經過的次數乘長度),即一條邊長為 \(l\),子樹內有 \(s\) 個關鍵點的邊,貢獻為 \(l\times s\times(k-s)\)。
假設每個子樹內的點數都不超過 \(\lfloor\frac k2\rfloor\),則按深度從大到小加點,若這是該子樹內第 \(i\) 次加點,其到根的距離為 \(d\),則這個點到根上所有邊的 \(s(k-s)\) 都會發生變化,也就是貢獻為:
\[d(i(k-i)-(i-1)(k-i+1))=d\times(k+1-2i) \]顯然當 \(i<\lfloor\frac k2\rfloor\) 時,其父親的貢獻比自己小,故把貢獻排序之後選最大的 \(k\) 個即可,然後考慮如果有子樹內的點數超過 \(\lfloor\frac k2\rfloor\) 怎麼處理,顯然這樣的子樹最多一個。
根據上面的貢獻式,可以得出在一個已經選出 \(\lfloor\frac k2\rfloor\) 個點的子樹內再選點是負貢獻,故只有在前面排序後不夠選出貢獻最大的 \(k\) 個時,才採用這種策略。同時由於選根的貢獻為 \(0\),故這時根一定要選上。
故設前面已經選出了 \(tot\) 個正貢獻的點,再選了一個根之後,還需要選出 \(k-tot-1\) 個點。與前面性質的證明類似,可以得出這 \(k-tot-1\) 個點必然是某個子樹中深度最淺的 \(k-tot-1\) 個,列舉這個子樹計算答案即可。
總複雜度 \(O(n\log 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\n", ans), 0;
}
1361E
題意
給定一個 \(n\) 個點 \(m\) 條邊的強連通有向圖,定義一個點 \(u\) 是好的當且僅當 \(u\) 到所有的節點存在唯一簡單路徑。
求出所有好點,特殊地,如果好點的個數嚴格小於 \(\frac n5\) 則輸出 \(-1\)。
\(1\le n\le 10^5\),\(0\le m\le 2\times10^5\),多測,所有資料的 \(n\) 之和不超過 \(10^5\),所有資料的 \(m\) 之和不超過 \(2\times10^5\)。
Solution
這是一道巧妙的圖論題。
先考慮一個點 \(u\) 是好的條件:以 \(u\) 為根的 DFS 樹唯一。判斷是否唯一隻需以 \(u\) 為根後跑出任意一棵 DFS 樹,判斷是否所有的非樹邊都是後代指向祖先即可,證明比較顯然。
考慮隨便找一個點判斷它是否是好的,隨機 \(100\) 次,如果找不到,就可以認為好的點數嚴格小於 \(\frac n5\),出錯的概率只有 \(2\times10^{-10}\)。
這樣就找到了任意一個好點 \(r\),先跑一棵生成樹,考慮一個點 \(u\ne 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;
}