【题解】P8816 [CSP-J 2022] 上升点列
Lucyna_Kushinada · · 题解
题目传送门
这道题很有意思,做法还蛮多的。
深搜(无优化)
我们可以先回归朴素,直接按题意去对每一个起点深搜,找到所有合法的点列路径。
#include<bits/stdc++.h>
using namespace std;
#define N 505
int n,p,ans;
bitset<N>f;//类似bool数组,在O2优化下效率比bool数组更高
struct pt{
int x,y;
}a[N];
bool cmp(pt x,pt y){//判断是否递增
if(x.x>y.x||x.y>y.y)return 0;
return 1;
}
void dfs(int k,int lp,int len){
for(int i=1;i<=n;i++){
if(!f[i]&&k!=i&&cmp(a[k],a[i])){//没选且不是自己且是递增的
int dis=a[i].x-a[k].x+a[i].y-a[k].y-1;
//两个点如果要直达需要放的点数
if(dis>lp)continue;
f[i]=1;//标记已经选过
dfs(i,lp-dis,len+dis+1);//继续dfs
f[i]=0;//回溯
}
}
ans=max(ans,len+lp);//当前数列长度加剩下没放的点数
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>p;
for(int i=1;i<=n;i++)
cin>>a[i].x>>a[i].y;
for(int i=1;i<=n;i++)
dfs(i,p,1);
cout<<ans;
return 0;
}
代码很精简,分数自然也不会高到哪里去。
最劣时间复杂度:
用时:
内存:
分数:
深搜(剪枝)
观察之前的朴素深搜,发现每次搜都要从
于是我们可以对每个点预处理出能与当前点组成上升点列的点,然后直接深搜就行了。
#include<bits/stdc++.h>
using namespace std;
#define N 505
#define dist(i,k) a[i].x-a[k].x+a[i].y-a[k].y-1//曼哈顿距离-1
int n,p,ans;
bitset<N>f;//类似bool数组,在O2优化下效率比bool数组更高
struct pt{
int x,y;
vector<int>e;//存储能组成上升点列的点
}a[N];
bool cmp(pt x,pt y){//判断是否递增
if(x.x>y.x||x.y>y.y)return 0;
return 1;
}
void dfs(int k,int lp,int len){
f[k]=1;//选择
for(int i:a[k].e){//逐点遍历
if(!f[i]&&k!=i&&cmp(a[k],a[i])){//没选且不是自己且是递增的
int dis=dist(i,k);
//两个点如果要直达需要放的点数
if(dis>lp)continue;
dfs(i,lp-dis,len+dis+1);//继续dfs
}
}
ans=max(ans,len+lp);//当前数列长度加剩下没放的点数
f[k]=0;//回溯
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>p;
for(int i=1;i<=n;i++)
cin>>a[i].x>>a[i].y;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j||a[i].x>a[j].x||a[i].y>a[j].y||dist(j,i)>p)continue;
//如果是同一个点或者无法组成上升点列或距离大于k就跳过
a[i].e.push_back(j);
}
}
for(int i=1;i<=n;i++)
dfs(i,p,1);
cout<<ans;
return 0;
}
实质上就是剪了一些枝,大体还是没什么变化的。
最劣时间复杂度:
用时:
内存:
分数:
动态规划(最长不下降子序列)
我们可以发现,当
如果有的数据点答案是在其最长不下降子序列后面添上
#include<bits/stdc++.h>
using namespace std;
#define N 505
int n,p,ans,dp[N];
struct pt{
int x,y;
}a[N];
bool cmp(pt x,pt y){
if(x.x!=y.x)return x.x<y.x;
return x.y<y.y;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>p;
for(int i=1;i<=n;i++){
cin>>a[i].x>>a[i].y;
dp[i]=1;//初始化为1
}
sort(a+1,a+1+n,cmp);//按坐标排序保证dp顺序
for(int i=1;i<=n;i++){
for(int j=1;j<n;j++){
if((a[i].x-a[j].x==1&&a[i].y==a[j].y)||(a[i].y-a[j].y==1&&a[i].x==a[j].x))
//判断是否递增
dp[i]=max(dp[i],dp[j]+1);
}
}
for(int i=1;i<=n;i++)
ans=max(ans,dp[i]);
cout<<ans+p;
return 0;
}
改下判断递增的部分然后套模板即可,代码非常精简。
最劣时间复杂度:
用时:
内存:
分数:
广搜
我们观察数据范围,可以发现有很多的数据点满足
搜的方向只有右与上方,满足题意,当走到没点的位置时减去剩余可放置点数,否则不减,相当于点权有两种,所以可以当 deque。
#include<bits/stdc++.h>
using namespace std;
#define N 505
int n,p,ans,dx[]={1,0},dy[]={0,1};
int x[N],y[N];
bool mp[105][105],v[105][105];
void bfs(int x,int y){
memset(v,0,sizeof(v));//初始化
deque<int>qx,qy,ql,qs;
v[x][y]=1;
qx.push_back(x);qy.push_back(y);
ql.push_back(p);qs.push_back(1);
while(!qx.empty()){
int xx=qx.front(),yy=qy.front();
int lp=ql.front(),st=qs.front();
ans=max(ans,st+lp);//路径长度+剩余点
qx.pop_front();qy.pop_front();
ql.pop_front();qs.pop_front();
for(int i=0;i<2;i++){
int tx=xx+dx[i],ty=yy+dy[i];
if(tx>100||ty>100||v[tx][ty])continue;
v[tx][ty]=1;
if(mp[tx][ty]){//位置上有点,加入到队头
qx.push_front(tx);qy.push_front(ty);
ql.push_front(lp);qs.push_front(st+1);
}
else{//位置上没点,剩余点减去1,并加入到队尾
if(lp==0)continue;//没点能用了就跳过
qx.push_back(tx);qy.push_back(ty);
ql.push_back(lp-1);qs.push_back(st+1);
}
}
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>p;
for(int i=1;i<=n;i++){
cin>>x[i]>>y[i];
mp[x[i]][y[i]]=1;
}
for(int i=1;i<=n;i++)
bfs(x[i],y[i]);//对每个起点都BFS
cout<<ans;
return 0;
}
双端队列
最劣时间复杂度:
用时:
内存:
分数:
动态规划(对坐标递推)
我们令
不难推出,当
当
#include<bits/stdc++.h>
using namespace std;
#define N 505
int n,p,ans,x[N],y[N];
int cost[105][105],dp[105][105];
bool mp[105][105];
inline void upd1(int tx,int ty,int add){//从(x-1,y)转移
//当(tx,ty)没点时,add为1,代表多消耗一个点
dp[tx][ty]=dp[tx-1][ty]+1;
cost[tx][ty]=cost[tx-1][ty]+add;
}
inline void upd2(int tx,int ty,int add){//从(x,y-1)转移
dp[tx][ty]=dp[tx][ty-1]+1;
cost[tx][ty]=cost[tx][ty-1]+add;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>p;
for(int i=1;i<=n;i++){
cin>>x[i]>>y[i];
mp[x[i]][y[i]]=1;
}
for(int i=1;i<=n;i++){//对每个点都dp
memset(cost,0,sizeof(cost));
memset(dp,0,sizeof(dp));//初始化
for(int tx=x[i];tx<=100;tx++){
for(int ty=y[i];ty<=100;ty++){
if(mp[tx][ty]){//有点
if(dp[tx-1][ty]>dp[tx][ty-1])
upd1(tx,ty,0);
else if(dp[tx-1][ty]<dp[tx][ty-1])
upd2(tx,ty,0);
else
if(cost[tx-1][ty]<cost[tx][ty-1])
upd1(tx,ty,0);
else
upd2(tx,ty,0);
}
else{//没有点
if(cost[tx-1][ty]<p&&cost[tx][ty-1]<p){
if(dp[tx-1][ty]>dp[tx][ty-1])
upd1(tx,ty,1);
else if(dp[tx-1][ty]<dp[tx][ty-1])
upd2(tx,ty,1);
else
if(cost[tx-1][ty]<cost[tx][ty-1])
upd1(tx,ty,1);
else
upd2(tx,ty,1);
}
else{
if(cost[tx-1][ty]<p)
upd1(tx,ty,1);
if(cost[tx][ty-1]<p)
upd2(tx,ty,1);
//若都没有剩余可放置的点了干脆不转移,此时的dp[x][y]也满足定义。
}
}
ans=max(ans,dp[tx][ty]+p-cost[tx][ty]);//路径长度+剩余点
}
}
}
cout<<ans;
return 0;
}
本质上就是从
最劣时间复杂度:
用时:
内存:
分数:
图论最短路(Dijkstra单源最短路)
我们考虑建图,满足组成上升点列的两个点之间连一条边,边权为其曼哈顿距离减
于是问题就转化成了求点到点之间最少要放的点数,放的越少,能直接放到后面的点就越多,答案也就越优。
#include<bits/stdc++.h>
using namespace std;
#define N 505
#define pr pair<int,int>
#define dist first
#define id second
#define gdis(i,k) (a[i].x-a[k].x+a[i].y-a[k].y-1)//曼哈顿距离-1
int n,k,ans,dis[N];
bitset<N>f;//类似bool数组,在O2优化下效率比bool数组更高
struct edge{//边结构体
int t,w;
edge(int a,int b){t=a,w=b;}
};
struct pt{
int x,y;
}a[N];
vector<edge>e[N];
void dijkstra(int s){//单源最短路算法
for(int i=1;i<=n;i++)dis[i]=INT_MAX;
dis[s]=0;
f.reset();
priority_queue<pr,vector<pr>,greater<pr>>q;
q.push({dis[s],s});
while(!q.empty()){
pr u=q.top();
q.pop();
if(f[u.id])continue;
f[u.id]=1;
for(edge x:e[u.id]){
if(u.dist+x.w<dis[x.t]){
dis[x.t]=u.dist+x.w;
q.push({dis[x.t],x.t});
}
}
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>a[i].x>>a[i].y;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j||a[i].x>a[j].x||a[i].y>a[j].y||gdis(j,i)>k)continue;//不符合要求跳过
e[i].push_back(edge(j,gdis(j,i)));//建边
}
}
for(int i=1;i<=n;i++){
dijkstra(i);
for(int j=1;j<=n;j++){
if(dis[j]>k)continue;//最少要放的点数大于k跳过
ans=max(ans,gdis(j,i)+1+k-dis[j]+1);//路径长度加剩余点数
}
}
cout<<ans;
return 0;
}
建完边直接套
最劣时间复杂度:远远小于
复杂度证明:
用时:
内存:
分数:
图论最短路(Floyd全源最短路&动态规划)
观察数据范围,节点数最多只有
#include<bits/stdc++.h>
using namespace std;
#define N 505
#define gdis(i,k) (a[i].x-a[k].x+a[i].y-a[k].y-1)//曼哈顿距离-1
int n,k,ans,f[N][N];
struct pt{
int x,y;
}a[N];
void floyd(){//全源最短路算法
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
if(f[i][k]!=(INT_MAX>>1))//剪枝,判断i能否到k,不然提前退出
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>a[i].x>>a[i].y;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j]=i==j?0:INT_MAX>>1;//初始化
if(i==j||a[i].x>a[j].x||a[i].y>a[j].y||gdis(j,i)>k)continue;//不符合要求跳过
f[i][j]=gdis(j,i);//建边
}
}
floyd();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(f[i][j]<=k)ans=max(ans,gdis(j,i)+1+k-f[i][j]+1);
cout<<ans;
return 0;
}
建完边直接套
最劣时间复杂度:
用时:
内存:
分数:
动态规划(对点递推)
我们令
于是
#include<bits/stdc++.h>
using namespace std;
#define N 505
#define K 105
#define dist(i,k) (a[i].x-a[k].x+a[i].y-a[k].y-1)//曼哈顿距离-1
int n,k,ans,dp[N][K];
struct pt{
int x,y;
}a[N];
bool cmp(pt x,pt y){
if(x.x!=y.x)return x.x<y.x;
return x.y<y.y;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)
cin>>a[i].x>>a[i].y;
sort(a+1,a+1+n,cmp);//按坐标排序,保证DP顺序正确
for(int p=0;p<=k;p++)
for(int i=1;i<=n;i++)
dp[i][p]=1;//初始化为1
for(int p=0;p<=k;p++){
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
if(a[j].y>a[i].y||dist(i,j)>p)continue;//判断合法
dp[i][p]=max(dp[i][p],dp[j][p-dist(i,j)]+dist(i,j)+1);
//状态转移
}
}
}
for(int p=0;p<=k;p++)
for(int i=1;i<=n;i++)
ans=max(ans,dp[i][p]+k-p);//路径长度+剩余点数
cout<<ans;
return 0;
}
代码出奇的精简,但是有些细节,难度基本都在推式子上面。
最劣时间复杂度:
用时:
内存:
分数: