11-1模擬賽 By cellur925
期望得分:70+100+60
實際得分:70+20+60 \(qwq\)。
T1:有一個 \(n\) × \(n\) 的 \(01\) 方格, 圖圖要從中選出一個面積最大的矩形區域, 要求這個矩形區域不能有超過 \(k\) 個 \(1\)。
開始只會\(O(n^4)\)演算法,即列舉左上角和右下角,然後去寫了T2&T3,回來想了一個多小時大概,還是沒想出...但是造了大資料驗證了一下暴力應該是沒問題的。
70分暴力:
#include<cstdio> #include<algorithm> using namespace std; int n,k,ans; int mapp[600][600]; int main() { freopen("game.in","r",stdin); freopen("game.out","w",stdout); scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&mapp[i][j]); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) mapp[i][j]+=mapp[i-1][j]+mapp[i][j-1]-mapp[i-1][j-1]; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) for(int l=1;l<=n&&i-l+1>=1;l++) for(int r=1;r<=n&&j-r+1>=1;r++) { int ii=i-l+1; int jj=j-r+1; int qwq=mapp[i][j]+mapp[ii-1][jj-1]-mapp[ii-1][j]-mapp[i][jj-1]; if(qwq>k) continue; ans=max(ans,l*r); } printf("%d",ans); return 0; }
正解:很巧妙的想法:我們不需列舉那麼多,有些是重複的。因為矩陣的二維字首和是有單調性的。什麼意思?我們列舉矩陣的上下界,然後只列舉一個右邊界,左邊界從1開始向右推進,顯然在開始時刻包含的1最多,在不斷向右推進的過程中包含的1越來越少,當找到一個滿足\(k\)的時刻我們便停止向右推進,這時一定保證最優。因為右面的情況會越來越少。
#include<cstdio> #include<algorithm> using namespace std; int n,k,ans; int f[1000][1000]; int ask(int a,int b,int c,int d) { return f[c][d]+f[a-1][b-1]-f[a-1][d]-f[c][b-1]; } int main() { scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&f[i][j]); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) f[i][j]+=f[i-1][j]+f[i][j-1]-f[i-1][j-1]; for(int i=1;i<=n;i++) for(int j=i;j<=n;j++) { int l=1; for(int r=1;r<=n;r++) { while(ask(i,l,j,r)>k) l++; ans=max(ans,(r-l+1)*(j-i+1)); } } printf("%d\n",ans); return 0; }
主要是這個單調性,它就是很顯然的東西,但是如果不注意它的話就真找不到正解==。
T2:他正在玩一款策略遊戲, 地圖由 \(n\) 座城市組成, 並由 \(n - 1\) 條無向帶權邊連線成樹形結構。 為了解決物資補給, 圖圖需要在這 \(n\) 座城市選出若干座城市建立機場, 其中在第 \(i\) 座城市建立機場的代價是 \(cost[i]\)。
建立機場之後, 每座城市得到補給的代價為該城市到最近機場的距離。 而他總共花費的代價即為建立機場的代價與每座城市得到補給的代價之和, 當然他想讓這個代價最小。
考場上讀完題後感覺是自己做過的原題==然後就慘慘了。事後感覺一點也不一樣啊,只是看起來很像⑧了(摔)。事實是考場上幾乎不會出原題的,如果感覺像一定要一再嘗試否認自己==。不能盲目自信==。
這種最優化&在樹上背景,大多是樹形dp。我竟然從沒向這想過(。首先我們考慮一條鏈的情況,\(f[i]\)表示前\(i\)個城市的代價均已計算,最後一個機場設在第\(i\)個城市的最小花費。轉移:
https://cdn.luogu.org/upload/pic/41397.png
如果狀態設計出來,這個轉移應該還是不難理解的。考慮如何擴充套件到樹的情況,一般我們設計樹形dp的狀態時,有諸如\(dp[i]\)為以\(i\)為根的子樹\(balabala\)...的表述。
我們考慮設計這樣一個狀態:\(f[i][j]\)表示以\(i\)為根的子樹,當前距離\(i\)最近的機場設在了\(j\),子樹內所有城市花費之和的最小值,另設一個\(g[i]\)儲存\(min{f[i][j]}\)。那麼轉移:有兩部分我們是一定要花費的,\(i\)到\(j\)的距離和在\(j\)點建造的花費。其他:列舉\(i\)子樹內的所有兒子\(v\),在\(g[v]\)和\(f[v][j]\)-\(val[j]\)中尋求最小。(減去\(val[j]\)是為了防止算多。)那麼轉移即為這樣:
for(int j=1;j<=n;j++)
{
f[u][j]=val[j]+dis[u][j];
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v!=fa) f[u][j]+=min(g[v],f[v][j]-val[j]);
}
}
可以看出我們還需要計算一下兩點間的距離:因為是在一棵樹上,因為複雜度被控制在不\(n^2\)內,那麼我們可以從每個點出發進行一遍\(dfs\)求出這個距離。(感覺\(std\)的這個想法好巧妙啊x)
for(int i=1;i<=n;i++) dfs(i,0,i);
void dfs(int u,int fa,int s)
{
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==fa) continue;
dis[s][v]=dis[s][u]+edge[i].val;
dfs(v,u,s);
}
}
完整程式碼
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 2600
using namespace std;
int n,tot;
int head[maxn],val[maxn],g[maxn],dis[maxn][maxn],f[maxn][maxn];
struct node{
int to,next,val;
}edge[maxn<<1];
void add(int x,int y,int z)
{
edge[++tot].to=y;
edge[tot].next=head[x];
head[x]=tot;
edge[tot].val=z;
}
void dfs(int u,int fa,int s)
{
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v==fa) continue;
dis[s][v]=dis[s][u]+edge[i].val;
dfs(v,u,s);
}
}
void TreeDP(int u,int fa)
{
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v!=fa) TreeDP(v,u);
}
for(int j=1;j<=n;j++)
{
f[u][j]=val[j]+dis[u][j];
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(v!=fa) f[u][j]+=min(g[v],f[v][j]-val[j]);
}
}
g[u]=f[u][1];
for(int i=1;i<=n;i++) g[u]=min(g[u],f[u][i]);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&val[i]);
for(int i=1;i<=n-1;i++)
{
int x=0,y=0,z=0;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z),add(y,x,z);
}
for(int i=1;i<=n;i++) dfs(i,0,i);
TreeDP(1,0);
printf("%d\n",g[1]);
return 0;
}
T3:他計劃去 \(Bzeroth\) 的精靈王國去旅遊, 精靈王國由 \(n\) 座城市組成, 第 \(i\) 座城市有 \(3\) 個屬性 \(x[i]\), \(w[i]\), \(t[i]\)。
在精靈王國的城市之間穿行只能依靠傳送陣, 第 \(i\) 座城市的傳送陣可以將他從城市 \(i\) 傳送到距離城市 \(i\) 不超過 \(w[i]\)的任意一個城市, 並需要 \(t[i]\)的時間完成傳送。 現在他知道了每個城市的座標 \(x[i]\), 想知道他從城市 \(s\) 到城市 \(t\) 的最小時間。
唔...讀完題後感覺只會打\(60\)分的暴力,建邊最壞複雜度\(O(n^2)\),跑\(dijistra\)複雜度\(O(mlogn)\),總複雜度近似為\(O(n^2logn)\)。
\(60\)分暴力:
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
#define maxn 153000
using namespace std;
typedef long long ll;
int n,s,t,tot;
int pos[maxn],dist[maxn],w[maxn],head[maxn];
ll dis[maxn];
bool vis[maxn];
struct node{
int to,next,val;
}edge[6300000];
void add(int x,int y,int z)
{
edge[++tot].to=y;
edge[tot].next=head[x];
head[x]=tot;
edge[tot].val=z;
}
void dijkstra()
{
priority_queue<pair<ll,int> >q;
q.push(make_pair(0,s));
for(int i=1;i<=n;i++) dis[i]=2e9;
dis[s]=0;
while(!q.empty())
{
int u=q.top().second;q.pop();
if(vis[u]) continue;
vis[u]=1;
for(int i=head[u];i;i=edge[i].next)
{
int v=edge[i].to;
if(dis[v]>dis[u]+edge[i].val)
{
dis[v]=dis[u]+edge[i].val;
q.push(make_pair(-dis[v],v));
}
}
}
}
int main()
{//60 pts
freopen("trip.in","r",stdin);
freopen("trip.out","w",stdout);
scanf("%d%d%d",&n,&s,&t);
for(int i=1;i<=n;i++) scanf("%d",&pos[i]);
for(int i=1;i<=n;i++) scanf("%d",&dist[i]);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=1;i<=n;i++)
{
int j=i+1;
while(pos[j]-pos[i]<=dist[i]&&j<=n)
{
add(i,j,w[i]);
j++;
}
j=i-1;
while(pos[i]-pos[j]<=dist[i]&&j>=1)
{
add(i,j,w[i]);
j--;
}
}
dijkstra();
printf("%lld\n",dis[t]);
return 0;
}
正解:這個複雜度的瓶頸其實就在建邊上,首先很顯然能建的邊是一段連續的區間,我們可以優化用二分求得。然後我們可以從各點向這些連續區間連邊!這就很像線段樹的查詢區間了,把這些區間線上段樹上維護,那麼從每個城市出發,會向\(O(logn)\)個區間連邊,邊數被降到了\(O(nlogn)\)。再跑\(dij\)就能穩過了。程式碼...寫不出來,因為不會動態開點線段樹(,先鴿了。