Task.1 走格子
題目大意:C和F在一個可以看作 \(N\times M\) 的矩陣的房間中,矩陣中的每一個元素可以是:
-
障礙:"#"
-
C或者F的起點:"C"或"F"
-
空區域:"."
C攜帶了一把傳送槍,每次C都可以:
-
花費一個單位時間移動到相鄰的空區域
-
不花費時間向上下左右之一的方向的牆壁上發射傳送門(傳送門最多只能同時存在兩扇,如果已經存在兩扇再發射一扇那最早出現的那扇會消失,一個位置不能存在兩扇傳送門)
-
花費一個單位時間進入相鄰牆壁上的傳送門中移動到另一個傳送門前方的格子中。
求C到F的最短時間。
數據范圍:\(1\leq N,M\leq 500\),保證地圖最外圍是障礙。
https://store.steampowered.com/app/400/Portal/
上來我們就想想爆搜?能過嗎?可以拿到85pts的好成績???
正確的操作是這樣的:當你在某個位置時,你可以向四個方向上發射兩個傳送門,一定可以走到那個最近的牆邊進傳送門到另外一面牆前面。
做法就很簡單了:bfs或最短路算法,考慮一下上述情況就可以了。
代碼:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
#include<queue>
#include<utility>
using namespace std;
template<class T>void read(T &x){
x=0; char c=getchar();
while(c<'0'||'9'<c)c=getchar();
while('0'<=c&&c<='9'){x=(x<<1)+(x<<3)+(c^48); c=getchar();}
}
typedef pair<int,int> pr;
const int N=505;
int n,m,s,t;
char mp[N][N];
vector<pr>e[N*N];
int num[N][N],L[N][N],R[N][N],U[N][N],D[N][N];
int dis[N*N];
priority_queue<pr>q;
bool vis[N*N];
int dx[5]={0,0,1,-1};
int dy[5]={1,-1,0,0};
void add(int x,int y,int w){e[x].push_back(pr(y,w));}
bool dijkstra(){
memset(vis,0,sizeof(vis));
memset(dis,0x7f,sizeof(dis));
dis[s]=0; q.push(pr(0,s));
pr now; int x,y,d;
while(!q.empty()){
now=q.top(); q.pop();
x=now.second; if(vis[x])continue; vis[x]=1;
for(int i=e[x].size()-1;~i;i--){
y=e[x][i].first; d=e[x][i].second;
if(dis[y]>dis[x]+d){
dis[y]=dis[x]+d;
if(!vis[y])q.push(pr(-dis[y],y));
}
}
}
return dis[t]!=dis[0];
}
int main(){
freopen("cell.in","r",stdin);
freopen("cell.out","w",stdout);
read(n); read(m); int cnt=0;
for(int i=1;i<=n;i++){
scanf("%s",mp[i]+1);
for(int j=1;j<=m;j++){
if(mp[i][j]=='#'){L[i][j]=R[i][j]=U[i][j]=D[i][j]=-1; continue;}
num[i][j]=++cnt;
if(mp[i][j]=='C') s=num[i][j];
if(mp[i][j]=='F') t=num[i][j];
L[i][j]=L[i][j-1]+1; U[i][j]=U[i-1][j]+1;
}
}
for(int i=n;i;i--) for(int j=m;j;j--) if(mp[i][j]!='#'){R[i][j]=R[i][j+1]+1; D[i][j]=D[i+1][j]+1;}
int nx,ny,d;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++) if(mp[i][j]!='#'){
for(int k=0;k<4;k++){
nx=i+dx[k]; ny=j+dy[k];
if(mp[nx][ny]=='#')continue;
add(num[i][j],num[nx][ny],1);
}
d=min(min(L[i][j],R[i][j]),min(U[i][j],D[i][j]))+1;
add(num[i][j],num[i][j-L[i][j]],d);
add(num[i][j],num[i][j+R[i][j]],d);
add(num[i][j],num[i-U[i][j]][j],d);
add(num[i][j],num[i+D[i][j]][j],d);
}
}
if(dijkstra()) printf("%d\n",dis[t]);
else puts("wtf");
return 0;
}
Task.2 扭動的樹
題目大意:有一棵 \(key\) 為鍵值 \(val\) 為權值 \(n\) 個點的二叉查找樹,定義某個節點的 \(sum\) 值為它的子樹內的 \(val\) 的和。告訴你 \(n\) 個節點的 \(key\) 值和 \(val\) 值,求滿足樹上任意一條邊兩個端點 \(key\) 值最大公約數不為 \(1\) 時,樹上所有節點的 \(sum\) 值的和最大是多少。
數據范圍:\(1\leq N\leq 300,1\leq key_i\leq 10^18,1\leq val_i\leq 10^6\)
一棵二叉搜索樹的中序遍歷就是所有鍵值排序后的結果。試着排序后的 \(n\) 個元素進行合並,可以使用類似區間DP的思路,定義狀態 \(f_{i,j,k}\) 表示區間 \([i,j]\) 合並成一棵子樹根為 \(k\) 的最大答案,轉移類似區間DP,復雜度 \(O(n^4)\)。
對這個思路進行優化:區間 \([i,j]\) 以 \(k\) 為根的情況最終一定可以由 \([i,k-1]\) 和 \([k+1,j]\) 得到。所以我們試着把上述DP的第三維改進一下,定義 \(f_{i,j,0/1}\) 表示區間 \([i,j]\) 以 \(i-1\) 或 \(j+1\) 為根的答案,轉移依然是類似區間DP的,枚舉根 \(k\),復雜度 \(O(N^3)\)。
代碼:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<utility>
using namespace std;
template<class T>void read(T &x){
x=0; char c=getchar();
while(c<'0'||'9'<c)c=getchar();
while('0'<=c&&c<='9'){x=(x<<1)+(x<<3)+(c^48); c=getchar();}
}
typedef long long ll;
typedef pair<ll,ll> pr;
const int N=305;
#define key first
#define val second
int n;
pr a[N];
ll sum[N],f[N][N][2],ans;
bool e[N][N];
void cmax(ll &x,ll y){if(x<y)x=y;}
ll gcd(ll x,ll y){return y==0?x:gcd(y,x%y);}
int main(){
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
read(n);
for(int i=1;i<=n;i++){read(a[i].key); read(a[i].val);}
sort(a+1,a+n+1);
if(a[1].key==1){puts("-1"); return 0;}
memset(f,-0x3f,sizeof(f));
for(int i=1;i<=n;i++){
sum[i]=sum[i-1]+a[i].val;
for(int j=i+1;j<=n;j++)
e[i][j]=(gcd(a[i].key,a[j].key)!=1);
if(e[i-1][i]) f[i][i][0]=a[i].val;
if(e[i][i+1]) f[i][i][1]=a[i].val;
}
for(int len=2;len<=n;len++)
for(int i=1,j=i+len-1;j<=n;i++,j++){
ll cost=sum[j]-sum[i-1];
for(int k=i;k<=j;k++){
ll add=(i<k)*f[i][k-1][1]+(k<j)*f[k+1][j][0]+cost;
if(e[i-1][k]) cmax(f[i][j][0],add);
if(e[k][j+1]) cmax(f[i][j][1],add);
if(len==n) cmax(ans,add);
}
}
printf("%lld\n",ans);
return 0;
}
Task.3 旋轉字段
題目大意:定義一個排列的價值是排列中 \(p_i=i\) 這樣的固定點的個數。現在和以選擇一個區間 \([L,R]\) 翻轉,求翻轉后最多的固定點。
數據范圍:\(1\leq N\leq 10^5\)
有這么一個性質:在位置 \(i\) 上的數字 \(x\) 關於 \(\frac{(i+x)}{2}\) 翻折會產生 \(1\) 的貢獻,並且某個位置上的數它會產生貢獻的對稱點都是固定的。再考慮一下最后答案翻轉的區間長什么樣子:區間左右端點至少有一個關於對稱點(區間中點)翻轉后會產生貢獻,否則如果選了這兩個端點一定不會是答案變優。
根據上述的分析,我們想到了一個貪心做法:把相同對稱點的點放在一起做,從小到達枚舉區間半徑,利用前綴和差分得到翻轉區間之外的答案,復雜度 \(O(N^2)\) 。還不夠好,我們發現枚舉半徑時有很多多余的情況,把這些情況去掉,每次由近至遠枚舉區間端點,跳過翻轉后不產生貢獻的點,計算答案方式和上面相同。這樣每個位置只被考慮一次,復雜度 \(O(N)\)。
代碼:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;
template<class T>void read(T &x){
x=0; char c=getchar();
while(c<'0'||'9'<c)c=getchar();
while('0'<=c&&c<='9'){x=(x<<1)+(x<<3)+(c^48); c=getchar();}
}
typedef pair<int,int> pr;
void cmax(int &x,int y){if(x<y)x=y;}
void cmin(int &x,int y){if(x>y)x=y;}
const int N=100050;
int n,a[N],pre[N],ans;
vector<int>q[N<<1];
int main(){
freopen("rotate.in","r",stdin);
freopen("rotate.out","w",stdout);
read(n);
for(int i=1;i<=n;i++){
read(a[i]); pre[i]=pre[i-1]+(a[i]==i);
if(a[i]>i) q[a[i]+i].push_back(a[i]);
else q[a[i]+i].push_back(i);
}
int L,R;
for(int i=(n<<1);i>1;i--){
sort(q[i].begin(),q[i].end());
for(int j=0;j<q[i].size();j++){
R=q[i][j]; L=i-R;
cmax(ans,pre[L-1]+pre[n]-pre[R]+j+1);
}
}
printf("%d\n",ans);
return 0;
}