2024.3.30 模擬賽

心海秋的墨木仄發表於2024-04-05

A 數列刪除

至少刪除 \(m\) 個數,意思就是最多保留 \(n-m\) 個數。

刪除的總和最小,意思就是保留的總和最大。

非降子序列問題可以用經典的動態規劃來解決。

\(f[i][j]\) 表示,當前選的最後一個數是 \(a[i]\),一共選了 \(j\) 個數,選的數總和最大是多少。

轉移就是列舉上一個數 \(a[k]\),滿足 \(k<i\&\&a[k]\leq a[i]\)\(f[i][j]\) 可以用 \(f[k][j-1]+a[i]\) 轉移。

#include<bits/stdc++.h>

using namespace std;

int f[1010][1010];
int a[1010];
int main() {
    int n, m, i, j, k, sum = 0, ans = 0;
    cin >> n >> m;
    for ( i = 1 ; i <= n ; i++)
        cin >> a[i], sum += a[i];
    
    for ( i = 1; i <= n; i++ ) {
        f[i][1] = a[i];
        for (j = 2; j <= n-m; j++)
            for (k = 1; k < i; k++)
                if (a[k] <= a[i])
                    f[i][j] = max(f[i][j], f[k][j-1]+a[i]);
        
        for (j = 1; j <= n-m; j++)
            ans=max(ans, f[i][j]);
    }

    cout<<sum-ans;
    

    return 0;
}

B 遊戲升級

對應題目資料範圍:

\(10pts\):輸出 \(0\) 即可。

\(20pts\):暴力即可。

\(20pts\)\(x>A_1\) 時小明無法升級,暴力列舉小於等於 \(A_1\)\(x\),大於 \(A_1\) 的整體處理。

\(20pts\):即 \(\lfloor \frac {A_1}x\rfloor=\lfloor \frac {A_2}x\rfloor\)。亂設的部分分,其實是提醒你考慮 \(\lfloor \frac {A_1}x\rfloor\) 的取值種類。

對於 \(100\%\) 的資料:只需要發現 \(\lfloor \frac {A_i}x\rfloor\) 的取值種類只有 \(O(\sqrt{A_i})\) 種。然後這題就沒了,可以用類似整除分塊的寫法求出有多少組這樣的取值。複雜度 \(O(T\sqrt{A})\)

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    int T;
    cin >> T;
    while (T--) {
        int a1, b1, a2, b2, n;
        cin >> a1 >> b1 >> a2 >> b2 >> n;
        int ans = 0;
        for (int l = 1, r, i; l <= a1 + 1; l = r + 1) {
            i = a1 / l;
            r = i ? a1 / i : 1e9;
            int j = i + b1 - b2;
            if (j < 0 || j > a2)
                continue;
            int l2 = a2 / (j + 1) + 1, r2 = j ? a2 / j : 1e9;
            // cerr<<"> "<<i<<" "<<j<<"  "<<l<<" "<<r<<"  "<<l2<<" "<<r2<<endl;
            l2 = max(l, l2);
            r2 = min(r, r2);
            r2 = min(n, r2);
            if (l2 <= r2)
                ans += r2 - l2 + 1;
        }
        cout << ans << '\n';
    }
}

C 難題

測試點 \(1,2\)

暴力列舉初始的 \(x,y\),然後不斷進行 \(x=x+y,y=x+y\) 判斷是否存在某一時刻 \(x=X\)

測試點 \(3,4\)

只列舉初始 \(x\) 的取值,設 \(y=k\),然後帶入 \(x=x+y,y=x+y\) 的過程中,每個時刻 \(x\) 的值是 \(ak+b\) 的形式,其中 \(a,b\) 是定值,然後就是判斷 \(ak+b=X\) 是否有 \(k\in [1,M]\) 的解了。

測試點 \(5,6\)

輸出上面的 \(a,b\) 可以發現 \(a=f_i,b=f_{i+1}\),其中 \(i\) 為奇數。

然後就是求 \(xf_i+yf_{i+1}=N(x\in [0,N],y\in [0,M])\) 的解的個數,因為 \(gcd(f_i,f_{i+1})=1\),可以用擴歐求一個特解 \(x_0,y_0\)。滿足條件的 \(x\equiv x_0~mod~f_{i+1},y\equiv y_0~mod~f_i\),因此可以找到 \(x\) 最大且滿足條件的解 \((x_1,y_1)\),和 \(x\) 最小且滿足條件的解 \((x_2,y_2)\),於是可以算出當前情況下,解的個數為 \(\frac{x_1-x_2}{f_{i+1}}\)。複雜度為 \(O(nlog^2X)\)

測試點 \(7-10\)

輸出每一組 \(x_0,y_0\),會發現 \(x_0=-f_{i-1},y_0=f_{i-2}\)。可用歸納法證明:

假設 \(-f_if_{i-1}+f_{i+1}f_{i-2}=1\) 成立,那麼

\[-f_{i+1}f_i+f_{i+2}f_{i-1}\\ =-f_{i+1}(f_{i-1}+f_{i-2})+(f_{i+1}+f_i)f_{i-1}\\ =-f_{i+1}f_{i-2}+f_if_{i-1}=-1 \]

所以 \(-f_{i+2}f_{i+1}+f_{i+3}f_i=1\)(注意上面列舉的 \(i\) 為奇數),然後就可以最佳化掉一個 \(log\)

關於細節

若沒開 __\(int128\),或沒判斷 \(x=0\),或只列舉了 \(70\) 個斐波那契數,會 \(WA\)

#include <bits/stdc++.h>
using namespace std;
template <typename T>
inline void read(T &x) {
    x = 0;
    short f = 1;
    char c = getchar();
    for (; c < '0' || c > '9'; c = getchar())
        if (c == '-')
            f = -1;
    for (; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ 48);
    x *= f;
    return;
}
#define LL long long
#define ll __int128
const int N = 2e5 + 5;
ll exgcd(ll a, ll b, ll &x, ll &y) {
    if (!b) {
        x = 1, y = 0;
        return a;
    }
    ll t = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return t;
}
ll get_inv(ll a, ll p) {
    ll x = 0, y = 0;
    ll t = exgcd(a, p, x, y);
    ll ans = x / t % p;
    return (ans + p) % p;
}
ll inv[N];
int main() {
    int T;
    read(T);
    ll a = 1, b = 1;
    int idx = 0;
    while (1) {
        if (a > 1e18)
            break;
        inv[++idx] = get_inv(a, b);
        a = a + b;
        b = a + b;
    }
    while (T--) {
        ll x, n, m;
        read(x), read(n), read(m);
        ll a = 1, b = 1;
        LL ans = 0;
        int idx = 0;
        if (x == 0) {
            puts("1");
            continue;
        }
        while (1) {
            if (a > x)
                break;
            ll tmp = x % b * inv[++idx] % b;
            ll X = tmp, Y = (x - tmp * a) / b;
            if (Y > m) {
                ll k = (Y - m) / a + ((Y - m) % a != 0);
                Y -= k * a, X += k * b;
                if (Y + a <= m)
                    Y += a, X -= b;
            }
            if (X >= 0 && X <= n && Y >= 0 && Y <= m)
                ans += min(Y / a, (n - X) / b) + 1;
            a = a + b;
            b = a + b;
        }
        printf("%lld\n", ans);
    }
}

D 迷宮逃亡

若知道從安全區 \(i\) 走到安全區 \(j\) 的最短路,並以此為邊權連邊,容易想到最小生成樹,詢問時回答路徑最大邊權。

可以只保留可能出現在最小生成樹上的邊。注意到如果從 \(x\)\(z\) 的最優路徑經過了 \(y\),那麼只要連上 \((x,y),(y,z)\),不需要連 \((x,z)\)。因此可以跑一個多源 \(bfs\),計算距離每個溼地最近的是哪個安全區,即每個安全區的管轄範圍(是一個連通塊)。如果兩個連通塊不接壤,中間至少間隔一個 \(y\),連邊是不優的。若有接壤就連邊。

注意到 \(bfs\) 過程中就可以找到所有接壤的地方,即擴充到一個更新過的位置,且所屬安全區不同。這樣連出來的邊數是 \(O(nm)\) 的,總時間複雜度 \(O((nm+p+q)logp)\)

#include <algorithm>
#include <cstdio>

#define M 2005
#define N 200005

using namespace std;

char a[M][M];
int R,C,en,d[N],f[N],b[M][M],qx[M*M],qy[M*M],px[M*M],py[M*M],ax[N][20],pre[N][20],head[N];
const int dx[4]={1,0,-1,0},
		  dy[4]={0,1,0,-1};
struct edge
{
	int v,w,nxt;
}e[N*2];

int find(int);
bool mrg(int,int);
void dfs(int,int);
void adde(int,int,int);
pair<int,int> lca(int,int);

int main()
{
	int i,k,r,n,q,x,y,u,v,tp,tq,nx,ny;
	scanf("%d %d %d %d",&R,&C,&n,&q);
	for(i=1;i<=R;++i)scanf("%s",a[i]+1);
	for(i=1;i<=n;++i)
	{
		scanf("%d %d",&x,&y);
		f[i]=b[px[i]=x][py[i]=y]=i;
	}
	tp=n;
	for(r=0;tp;++r)
	{
		tq=0;
		for(i=1;i<=tp;++i)
		{
			x=px[i];y=py[i];
			for(k=0;k<4;++k)
				if(b[nx=x+dx[k]][ny=y+dy[k]]&&mrg(b[x][y],b[nx][ny]))
					adde(b[x][y],b[nx][ny],r*2);
		}
		for(i=1;i<=tp;++i)
		{
			x=px[i];y=py[i];
			for(k=0;k<4;++k)
				if(b[nx=x+dx[k]][ny=y+dy[k]])
				{
					if(mrg(b[x][y],b[nx][ny]))
						adde(b[x][y],b[nx][ny],r*2+1);
				}
				else if(nx&&nx<=R&&ny&&ny<=C&&a[nx][ny]=='.')
				{
					b[nx][ny]=b[x][y];
					qx[++tq]=nx;qy[tq]=ny;
				}
		}
		for(i=1;i<=tq;++i)
		{px[i]=qx[i];py[i]=qy[i];}
		tp=tq;
	}
	for(i=1;i<=n;++i)if(!d[i])dfs(i,0);
	while(q--)
	{
		scanf("%d %d",&u,&v);auto k=lca(u,v);
		if(!k.first)puts("-1");
		else printf("%d\n",k.second);
	}
	return 0;
}
void dfs(int u,int fa)
{
	int i,v;
	d[u]=d[pre[u][0]=fa]+1;
	for(i=1;i<=18;++i)
	{
		pre[u][i]=pre[pre[u][i-1]][i-1];
		ax[u][i]=max(ax[u][i-1],ax[pre[u][i-1]][i-1]);
	}
	for(i=head[u];i;i=e[i].nxt)
		if((v=e[i].v)!=fa)
			{ax[v][0]=e[i].w;dfs(v,u);}
}
pair<int,int> lca(int u,int v)
{
	int i,ans=0;
	if(d[u]>d[v])swap(u,v);
	for(i=18;i>=0;--i)
		if(d[u]<=d[v]-(1<<i))
			{ans=max(ans,ax[v][i]);v=pre[v][i];}
	if(u==v)return {u,ans};
	for(i=18;i>=0;--i)
		if(pre[u][i]!=pre[v][i])
		{
			ans=max(ans,max(ax[u][i],ax[v][i]));
			u=pre[u][i];v=pre[v][i];
		}
	ans=max(ans,max(ax[u][0],ax[v][0]));
	return {pre[u][0],ans};
}
int find(int u){return u==f[u]?u:(f[u]=find(f[u]));}
bool mrg(int u,int v){u=find(u);v=find(v);f[v]=u;return u!=v;}
void adde(int u,int v,int w)
{
	e[++en].v=v;e[en].w=w;
	e[en].nxt=head[u];head[u]=en;
	swap(u,v);
	e[++en].v=v;e[en].w=w;
	e[en].nxt=head[u];head[u]=en;
}

E 點格遊戲

容易發現連通分量的形狀只可能是環或者鏈,先討論鏈的情況:
image
這是一條長度為 \(5\) 的鏈,若先手操作任意一條邊,會形如:
image
此時後手有兩種選擇:

  1. 填滿整條鏈,獲得等同於鏈長的分數,然後還要再操作一步,相當於刪除一條鏈,交換先後手。

  2. 把鏈填成只剩兩個相鄰的格子,獲得鏈長 \(-2\) 的分數,後手畫兩個格子中間的邊,接下來先後手不變。

第二種選擇圖示如下:
Image
環的情況類似,但如果後手想先後手順序不變,需要讓出四個格子。
image
還有一些特殊情況,例如長度為 \(1/2\) 的鏈,先手一定會優先從小到大操作這些鏈,每次操作讓後手獲得鏈上的格子,同時交換先後手。

至此流程已經明瞭:

  1. 先手選擇長度為 \(1/2\) 的鏈,後手獲得鏈上的格子,同時交換先後手。

  2. 先手選擇一條鏈/一個環,後手選擇交換先後手/先後手不變,獲得相應分數。

容易發現先手選的鏈/環一定是當前鏈/環中長度最小的,可以設 \(dp_{i,j}\) 表示當前只剩下最大的 \(i\) 條鏈,\(j\) 個環,此時遊戲分數的最大值是多少,可以從 \(dp_{i+1,j}\)\(dp_{i,j+1}\) 轉移過來。由於鏈的兩端一定在邊界處,最多隻有 \(n+m\) 條鏈,複雜度 \(O(nm(n+m))\)

#include <bits/stdc++.h>
using namespace std;
const int N=405,inf=1e9;
inline void Max(int &a,int b){if(a<b)a=b;}
inline bool cmp(int x,int y){return x>y;}
int g1[N][N],g2[N][N],n,m,id[N][N],f[N][N];
int sz[N],fa[N];
inline int find_fa(int x){
	return x==fa[x]?x:fa[x]=find_fa(fa[x]);
}
bool flag[N];
inline void merge(int x,int y){
	x=find_fa(x);y=find_fa(y);
	if(x!=y){
		fa[y]=x;sz[x]+=sz[y];
		flag[x]|=flag[y];
	}else{
		flag[x]=1;
	}
}
char s[N];
int b[N],cnt1,c[N],cnt2;
int main(){
	scanf("%d %d",&n,&m);
	int tot=0;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)id[i][j]=++tot,fa[tot]=tot,sz[tot]=1;
	for(int i=1;i<=n+1;++i){
		scanf("%s",s+1);
		for(int j=1;j<=m;++j)g1[i][j]=(s[j]=='1');
	}
	for(int i=1;i<=n;++i){
		scanf("%s",s+1);
		for(int j=1;j<=m+1;++j)g2[i][j]=(s[j]=='1');
	}
	for(int i=1;i<=n;++i)for(int j=1;j<=m;++j){
		if(i<n){
			if(!g1[i+1][j])merge(id[i][j],id[i+1][j]);
		}
		if(j<m){
			if(!g2[i][j+1])merge(id[i][j],id[i][j+1]);
		}
	}
	for(int i=1;i<=n;++i)for(int j=1;j<=m;++j){
		if(fa[id[i][j]]!=id[i][j])continue;
		if(!g1[i][j]||!g1[i+1][j]||!g2[i][j]||!g2[i][j+1]){
			if(flag[id[i][j]])b[++cnt1]=sz[id[i][j]];
			else c[++cnt2]=sz[id[i][j]];
		}
	}
	sort(b+1,b+1+cnt1,cmp);sort(c+1,c+1+cnt2,cmp);
	for(int i=1;i<=cnt1+cnt2;++i){
		for(int j=0,k;j<=cnt2&&j<=i;++j){
			k=i-j;
			if(k>cnt1)continue;
			f[j][k]=-inf;
			if(j){
				if(c[j]<=2)Max(f[j][k],-f[j-1][k]-c[j]);
				else Max(f[j][k],min(-f[j-1][k]-c[j],f[j-1][k]-c[j]+4));
			}
			if(k)Max(f[j][k],min(-f[j][k-1]-b[k],f[j][k-1]-b[k]+8));
		}
	}
	printf("%d\n",f[cnt2][cnt1]);
	return 0;
}```