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+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 點格遊戲
容易發現連通分量的形狀只可能是環或者鏈,先討論鏈的情況:
這是一條長度為 \(5\) 的鏈,若先手操作任意一條邊,會形如:
此時後手有兩種選擇:
-
填滿整條鏈,獲得等同於鏈長的分數,然後還要再操作一步,相當於刪除一條鏈,交換先後手。
-
把鏈填成只剩兩個相鄰的格子,獲得鏈長 \(-2\) 的分數,後手畫兩個格子中間的邊,接下來先後手不變。
第二種選擇圖示如下:
環的情況類似,但如果後手想先後手順序不變,需要讓出四個格子。
還有一些特殊情況,例如長度為 \(1/2\) 的鏈,先手一定會優先從小到大操作這些鏈,每次操作讓後手獲得鏈上的格子,同時交換先後手。
至此流程已經明瞭:
-
先手選擇長度為 \(1/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;
}```