1. 程式人生 > 實用技巧 >2020牛客暑假多校訓練二

2020牛客暑假多校訓練二

暫時更新前七道題(按難度排序)

D.Duration

題意:

求給出時間的秒差

題解:

統一轉換為秒,然後取相減的絕對值

時間複雜度:O(1)

C.Cover the Tree

題意:

給一棵無根樹,求最少的鏈覆蓋所有的邊,並輸出任意符合條件的結果集合

題解:

對於一顆樹而言,我們取入度為1的作為葉子,以其中任意一個大於1度的作為根結點。我們會發現,葉子結點到其父節點的邊是不會被重疊覆蓋,除非使用兩次該葉節點作為鏈的端點。所以要使得最小鏈覆蓋所有,即將葉子結點進行不重複的兩兩匹配,從而將樹完全覆蓋。所以鏈個數的下界即為 $⌈\frac{s}{2}⌉ $

而對於 \(n\leq2\) 的情況,結果顯然。而 \(n > 3\)

則是 取dfs序 進行排序,將前 \(n/2\) 化為一組,後 \(n/2\) 化為另一組,然後匹配

證明:

設某邊兒子結點所覆蓋的葉子結點範圍為 \([L,R]\)

如果在\([L,R]\) 範圍內的某葉子結點,與該範圍以外的某葉子結點相匹配,則此邊一定會被經過

· 如果 \(R\leq \frac{n}{2}\) 那麼此邊必定會被 \(R - R+\frac{n}{2}\) 覆蓋

· 如果 \(L> \frac{n}{2}\) 那麼此邊必定會被 \(L-\frac{n}{2} — L\) 覆蓋

· 如果\(R> \frac{n}{2} 且 L \leq \frac{n}{2}\)

則必定存在 \(x\in [L,n/2] 或者 [n/2+1,R] 與 L+\frac{n}{2} 和 R -\frac{n}{2} 匹配\)

從而使得此邊被覆蓋

否則,根的度數不為1,所以 l ≠ 1或者 r ≠ s 必有一個是滿足的;l ≠ 1可以得出,這條邊在l 1- l s/2+1; r ≠ s,這條邊被 l s/2- ls 覆蓋;

s點為奇數時,只需根據葉子結點再接一個節點

時間複雜度:O (n)

F.Fake Maxpooling

題意:

給出 \(n\times m\) 的矩陣,其中 \(A_{i,j}=lcm(i,j)\) ,給出 \(k\) 求出,所有大小為 \(k\) 的子矩陣中的最大值的和.

注意不是 "和的最大值"

範圍\(n,m,k (1≤n,m≤5000,1≤k≤min \{n,m \})\)

題解:

對於求在大矩陣中求 給出子矩陣的最值,有以下常見的方法

DP , 單調佇列,優先佇列,倍增ST表

已知 lcm(a,b) = a*b/gcd(a,b) ,又n,m<5000 ,gcd() 的 時間複雜度大約為 log(n) , 所以對於每個位置的值 :5000 x 5000 x log(5000) ~ 1e8

而這裡時間限制大概在3s左右,可能資料沒有卡好, \(O(nmlogn)\) 基本上沒有 \(TLE\)

題解中給出了 \(O(nm)\)\(A_{i,j}的gcd\) 的方法,利用倍數,類似埃式篩。對於篩出任何互質的 (i,j) ,以及其倍數

for(int i=1;i<=n;i++){
    for(int j=1;j<=m;j++){
        if(!gcd[i][j]){
            for(int k=1; k*i<=n&&k*j<=m;k++){
                gcd[i*k][j*k] = k, A[k*i][k*j] = i*j*k;//lcm
            }
        }
    }
}

方法一:即是標程的 【單調佇列】\(O(a*b)\)

類似滑動視窗,處理出在行列 範圍【i,i+k】的最大值,然後橫向維護,縱向維護一次即可

int a[5005][5005];//a為處理出來的 lcm(i,j)
int que[5005];//維護下標
int b[5005][5005];
void func(){
	long long sum = 0;
	for(int i=1;i<=n;i++){
		int l=1,r=1;
		for(int j=1;j<=m;j++){
			while(r>l && j-que[l] >= k) ++l;
			while(r>l && a[i][j]>a[i][que[r-1]]) --r;
			que[r++] = j;
			if(j>=k) b[i][j-k+1] = a[i][que[l]];
		}
	}
	//橫向於縱向維護方式一樣 
	for(int j=1;j<=m-k+1;j++){
		int l=1,r=1;
		for(int i=1;i<=n;i++){
			while(r>l && i-que[l] >= k) ++l;
			while(r>l && b[i][j]>b[que[r-1]][j]) --r;
			que[r++] = i;
			if(i>=k) sum += b[que[l]][j];
		}
	}
	cout<<sum<<endl;
}

方法二: 【遞推/倍增】\(O(a*b*log(k))\)

要求出範圍為 k*k 的矩陣最值,我們可以採用遞推的方法

遞推公式 \(maxv(i,j,k) 標識 以(i,j) 為左上角,大小為k的矩形最值\)

\(maxv(i,j,k)=max\{maxv(i,j,k-1), maxv(i+1,j+1,k-1), maxv(i+1,j,k-1), maxv(i,j+1,k-1)\}\)

這樣時間複雜度會達到 \(O(a*b*k)\) , 但這樣明顯會超時 最壞便是5000^3

由於 \(O(a*b)\) 為複雜度下限,所以主要考慮如何優化 k

我們可以考慮倍增的思想 (其實也是基於分治的思想):

\(maxv(i,j,k)=max\{maxv(i,j,k), maxv(i+2^{(k-1)},j+2^{(k-1)},k-1),\)

\(maxv(i,j+2^{(k-1)},k-1), maxv(i+2^{(k-1)},j,k-1) \}\)

其他就是主要注意一下關於處理範圍的取捨. 由於資料不是那麼嚴格所以 \(O(a*b*log(k))\) 能過

void func(){
	long long sum = 0;
	int lok;
	lok=floor(log2(k));
	for(int l=0;l<=lok-1;l++){
		for(int i=1;i+(1<<l)<=n;i++){
			for(int j=1;j+(1<<l)<=m;j++){
				a[i][j] = max(a[i][j], max(a[i+(1<<l)][j+(1<<l)], max(a[i+(1<<l)][j], a[i][j+(1<<l)])));//最後處理出來的大小為 2^lok
			}
		}
	}
	
	for(int i=1;i<=n-k+1;i++){
		for(int j=1;j<=m-k+1;j++){
			sum += max(a[i][j],max(a[i+k-(1<<lok)][j+k-(1<<lok)],max(a[i+k-(1<<lok)][j],a[i][j+k-(1<<lok)])));
		}
	}
	cout<<sum<<endl;
}

B.Boundary

題意:

給出在二維平面上的 \(n\) 個點,其中 \((0,0)\) 必定在該圓的邊界上,找到一個圓,使得在該圓邊界上的點數最多.

題解:

考慮暴力情況: 列舉兩個點,再列舉其他點是否再該圓上 (找出圓心,兩中弦垂線交點) \(O(n^3)\)

思路一:

利用同弧所對圓周角相等(充分條件),可以求出圓周角,判斷是否相同。

由於是充分條件,所以在圓周角相同時還需要增加 $\overrightarrow{OP} \times \overrightarrow{OA} >0 $

通過增加方向判斷可以得到同側的圓周角相等時對應同弧。最後取眾數即可

二維向量叉乘公式 \(a(x_1,y_1),b(x_2,y_2)\) , \(\overrightarrow{a} \times \overrightarrow{b}=(x_1*y_2-x_2*y_1)\) 交叉相乘再相減

思路二:

可以利用三點求圓心公式,求得圓心座標,以出現的最多次數作為結果

有圓上三點為


設圓的公式如下:
\(Ax^{2}+Ay^{2}+Bx+Cy+D=0\)
係數由如下行列式求得:

\(A=\begin{vmatrix} x_{1}& y_{1} & 1\\ x2& y2& 1\\ x3& y3& 1 \end{vmatrix}\)
\(=x_{1}(y2-y3)-y_{1}(x2-x3)+x2x3-x3x2\)

\(B=-\begin{vmatrix} x_{1}^{2}+y_{1}^{2}& y_{1} & 1\\ x_{2}^{2}+y_{2}^{2}& y_{2}& 1\\ x_{3}^{2}+y_{3}^{2}& y_{3}& 1 \end{vmatrix}\)
\(= (x_{1}^{2}+y_{1}^{2})(y_{3}-y_{2})+ (x_{2}^{2}+y_{2}^{2})(y_{1}-y_{3})+(x_{3}^{2}+y_{3}^{2})(y_{2}-y_{1})\)

\(C=\begin{vmatrix} x_{1}^{2}+y_{1}^{2}& x_{1} & 1\\ x_{2}^{2}+y_{2}^{2}& x_{2}& 1\\ x_{3}^{2}+y_{3}^{2}& x_{3}& 1 \end{vmatrix}\)
\(=(x_{1}^{2}+y_{1}^{2})(x_{2}-x_{3})+ (x_{2}^{2}+y_{2}^{2})(x_{3}-x_{1})+(x_{3}^{2}+y_{3}^{2})(x_{1}-x_{2})\)

\(D=-\begin{vmatrix} x_{1}^{2}+y_{1}^{2}& x_{1} & y_{1}\\ x_{2}^{2}+y_{2}^{2}& x_{2}& y_{2}\\ x_{3}^{2}+y_{3}^{2}& x_{3}& y_{3} \end{vmatrix}\)
\(=(x_{1}^{2}+y_{1}^{2})(x_{3}y_{2}-x_{2}y_{3})+ (x_{2}^{2}+y_{2}^{2})(x_{1}y_{3}-x_{3}y_{1})+(x_{3}^{2}+y_{3}^{2})(x_{2}y_{1}-x_{1}y_{2})\)

將圓方程化為標準方程:
\((x-(-\frac{B}{2A}))^2+(y-(-\frac{C}{2A})^2)=(\sqrt{\frac{B^{2}+C^{2}-4AD}{4A^{2}}})^{2}\)

將上述係數代入即可解得圓心和半徑:
\(x=-\frac{B}{2A} = \frac{(x_{1}^{2}+y_{1}^{2})(y_{3}-y_{2})+ (x_{2}^{2}+y_{2}^{2})(y_{1}-y_{3})+(x_{3}^{2}+y_{3}^{2})(y_{2}-y_{1})}{2\times(x_{1}(y2-y3)-y_{1}(x2-x3)+x2x3-x3x2)}\)

\(y=-\frac{C}{2A} = -\frac{(x_{1}^{2}+y_{1}^{2})(x_{2}-x_{3})+ (x_{2}^{2}+y_{2}^{2})(x_{3}-x_{1})+(x_{3}^{2}+y_{3}^{2})(x_{1}-x_{2})}{2\times(x_{1}(y2-y3)-y_{1}(x2-x3)+x2x3-x3x2)}\)

\(r=\sqrt{\frac{B^{2}+C^{2}-4AD}{4A^{2}}}\)

Code:

const int maxn = 2e6+5;
struct Node{
    double x,y;
}node[maxn];
int ans ;

map<pair<double,double>,int>mp;
//由於其中一個點為(0,0)

void solve(Node a,Node b){ 
    double x = ((a.x*a.x+a.y*a.y)*(b.y)-(b.x*b.x+b.y*b.y)*(a.y))/(2.0*(a.x*b.y-a.y*b.x));
    double y = ((a.x*a.x+a.y*a.y)*(b.x)-(b.x*b.x+b.y*b.y)*(a.x))/(2.0*(a.y*b.x-a.x*b.y));
    mp[{x,y}]++;
    ans=max(ans,mp[{x,y}]);
}

int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>node[i].x>>node[i].y;
    }
    
    for(int i=1;i<=n;i++){
        mp.clear();
        for(int j=i+1;j<=n;j++){
            if(node[i].x*node[j].y==node[i].y*node[j].x) continue; //不能關於原點對稱
            solve(node[i],node[j]);
        }
    }
    cout<<ans+1<<endl;
    return 0;
}

J.Just Shuffle

題意:

給出長為 \(n\) 的排列 以及一個整數 \(k\) , 你要找到一個置換排列 \(P\) 是的通過對\(\{1,2,...,n\}\) 進行\(k\) 次置換操作,得到 \(A\) 排列。如果存在 \(P\) 則輸出任意,否則輸出 \(-1\) . (輸出的置換規律P即為 對\(\{1,2,...,n\}\)進行第一次置換的結果)

範圍:\(n,k (1≤n≤10^5,10^8≤k≤10^9)\)

題解:

這裡涉及到 置換群 以及 逆元

參考下面部落格的內容:

https://blog.csdn.net/qq_36102055/article/details/107456175

置換概念

對於一個n個元素集合S={a1,a2...an},排列P={p1,p2...pn},將排列P每個元素當做S的下標替換掉原本S每個元素的位置,就得到一個置換\(f=\{a_{p1},a_{p2}...a_{pn}\}\)

通過多次置換,可以發現規律:

排列 A=(2,3,1,5,4)

1.置換是可以分成塊的:也就是(2,3,1)和(4,5)這兩個塊,你可以發現無論怎麼置換都不會發生這兩個集合的元素之間互換。
2.存在迴圈:也就是對於每個塊,在置換一定次數之後就會變回原樣,比如(2,3,1)這個塊在置換三次後就會變回原來排列中的位置
3.每個環的(下面不叫塊了叫環)迴圈的大小就是塊的元素的數目

可以證明所有的置換都可以拆成若干個環w1,w2...wn, 而每個環的大小為r1,r2...rn,也就是說對於環wi在對原排列置換ri次後保持不變,那麼也就可以得到原排列置換lcm(r1,r2...rn)次後保持不變。

關係

\(C = lcm(r_1,r_2,..,r_n)\)

\(A^C = A\)

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define ll long long
#define pb push_back
#define eps 1e-8
#define endl '\n'
using namespace std;
const ll maxn = 1e6 + 5;
int n,k,a[maxn],ans[maxn];
bool vis[maxn];
vector<int> v;
void setinv(){
    int r = v.size(),inv;//r為環的大小,inv為k在r下的逆元
    for(int i = 0; i < r; i++)
        if((ll) k * i % r == 1)inv = i;//求逆元
    for(int i = 0; i < r; i++)
        ans[v[i]] = v[(i + inv) % r];//轉inv次
}
int main(){
    scanf("%d %d",&n,&k);
    for(int i = 1; i <= n; i++)
        scanf("%d",a + i);
    for(int i = 1; i <= n; i++){
        if(!vis[i]){
            v.clear();
            int x = a[i];
            while(!vis[x]){//找環操作
                v.push_back(x);
                vis[x] = 1;
                x = a[x];
            }
            //找到環後就處理該環所有位置的值
            setinv();
        }
    }
    for(int i = 1; i <= n; i++)
        printf("%d ",ans[i]);
    return 0;
}

A.All with Pairs

題意:

給定n個字串,求所有字串最長的字首與字尾相等長度的平方和。
如樣例,匹配長度為1,2,3的分別有4,4,1個,所以答案為\(4×1^2+4×2^2+1×3^2=29\)

範圍: \(n,(1≤n≤10^5)\) \(\sum|s_i|\leq10^6\)

題解:

首先思考暴力的方法 n 為個數,m為長度

\(O(n\times m)\)利用map記錄字尾出現次數​ + \(O(n^2)\) 列舉兩個點 x \(O(m)\) 遍歷字首長度\計算總和

這個複雜度快 \(O(n^3)\) 了,所以需要思考如何簡化複雜度

考慮使用 進位制hash 求出所有的字尾值,並保存於map中(相對map 好點)

\(O(n \times m)\) 求出字尾出現次數 , 再 \(O(n \times m)\)遍歷n個字串的字首值,出現與字尾相同則相加。但是原題中要求求出最長匹配,所以要減去前面多加的長度。可以使用 next[] 陣列

例如 \(aba\)\(aba\) 匹配的時候:

能夠匹配到 \(a,aba\) 由於要求最長的作為 貢獻值,所以一開始 加上 \(a\) 的貢獻之後就需要被剪掉。

我們知道 next[] 陣列記錄上一次前綴出現相同長度的位置,所以減去該位置長度即可

//給定n個字串,求所有字串最長的字首與字尾相等長度的平方和。
//如樣例,匹配長度為1,2,3的分別有4,4,1個,所以答案為
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int maxn = 1e6+5;
const ull mod = 998244353;
string str[maxn];
int nex[maxn];
map<ull,ull>Hsh;
const ull base = 131;
void getNext(string s){
    int i=0,j=-1;
    nex[i] = j;
    while(i<(int)s.length()){
        if(j==-1 || (s[i] == s[j])){
            i++,j++;
            nex[i] = j;
        }else j = nex[j];//向前匹配位置移動
    }
}
void getHash(string s){
    ull ans = 0;
    ull bas = 1;
    for(int i=s.length()-1;i>=0;i--){
        ans = (s[i]*bas + ans);
        bas *= base;
        Hsh[ans]++;
    }
}
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>str[i];
    ull ans = 0;
    for(int i=1;i<=n;i++) getHash(str[i]);
    for(int i=1;i<=n;i++){
        ull res = 0;
        getNext(str[i]);
        for(int j=0;j<(int)str[i].length();j++){
            res = (res*base+str[i][j]);
           // cout<<res<<" "<<Hsh[res]<<endl;
            ans = (ans + Hsh[res] * (j+1) % mod *(j+1) %mod)%mod ;//個數
            ans = (ans - Hsh[res] * nex[j+1] % mod *nex[j+1] % mod + mod)%mod;
            ans = (ans%mod+mod)%mod;
        }
    }
    cout<<ans<<endl;
    return 0;
}
    

G.Greater and Greater

題意:

給一個大小為 \(n\) 的序列 \(A\) , 和一個大小為 \(m\) 的序列 \(B\) ,求出長為 \(m\)\(A\) 中的子區間,使得\(S_i\geq B_i\) \(i\in\{1,2,..,m\}\)

範圍:

\(n,m (1≤n≤150000,1≤m≤min\{n,40000\}).\)

\(1\leq A_i,B_i\leq10^9\)

思路:

如果最粗暴的方式去匹配 複雜度為 \(0(nm)\) , 大概6e9 左右,那肯定超時了。

所以如何優化,或者通過什麼去記錄狀態,便於判斷?

我們可以用bitset維護狀態,用一個 bitsetf[m] , 維護\(A\) 的每個位置與比 \(b_j\) 的關係。

那麼要使得 子區間 S 的每一位大於 B 的每一位,只需要在對位上,所有的都符合大於 \(S_j>B_j\) 的關係即可。

#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+100;
bitset<N>ans,f;
struct node{
    int pos, x;
    bool operator < (const node &A) const {
        return x > A.x;
    }
}a[N],b[N];
int n,m;
int main(){
    scanf("%d%d",&n,&m);
    for (int i = 1; i <= n; ++i)
        scanf("%d",&a[i].x), a[i].pos = i;
    for (int i = 1; i <= m; ++i)
        scanf("%d",&b[i].x), b[i].pos = i;
    sort(a+1,a+n+1);
    sort(b+1,b+m+1);
    ans.set(); f.reset();

    for (int i = 1, j = 0; i <= m; ++i){
        while((j+1) <= n && a[j+1].x >= b[i].x){
            f[a[j+1].pos] = 1;
            j++;
        }
        ans &= (f >> b[i].pos);
    }
    printf("%d\n",(int)ans.count());
    return 0;
}

H.Happy Triangle

題意:

給出一個 multiset \(MS\) 和 q 次操作,\(MS\) 一開始是空的,有以下三種類型操作

1.插入 \(x\) 到 MS 中

2.刪除 \(x\) 在 MS 中

3.給出一個 \(x\) , 是否能在 MS 中找到兩個元素 a,b 使得與 \(x\) 一起構成 nondegenerate triangle

(三點不共線的三角形)

思路: