今天我们主要复习的是状压DP和tarjan的部分题,这都是我不会的呀......我也创造了集训以来我过题最少的记录,七道题我只AC了一道,一道数组开小了只有60分,一道求强连通分量代码写错了得了50分,其他的将近一百分都是水的......看来复习还是十分必要的。
其实这道题老姚讲过的:https://www.cnblogs.com/hbhszxyb/p/12710188.html
这道题我们首先注意到有限制的k最大只有20,这个数不是状压就是暴搜,而暴搜写不好很容易超时,于是考虑状压,我们对每个点跑一遍最短路不现实,但只对k+1个点跑是可以接受的,在输入限制时,可以用一个二进制数组压起来,在处理完以上所有的前备条件之后,我们来考虑如何DP,转移的过程我们可以想象到是从一个点的状态转移到一个可以转移(即满足限制)点,在所有的转移之后,我们可以对1~k+1每个点此时的距离加上它到n号节点的距离取最小值,这就是我们的答案了。
代码:
1 #include2 #include 3 #include 4 #include 5 const int N=(1<<20); 6 const int maxn=2e4+10; 7 using namespace std; 8 struct Node{ 9 int next,to,dis; 10 }edge[20*maxn]; 11 int Head[maxn],tot; 12 void Add(int x,int y,int z){ 13 edge[++tot].to=y; 14 edge[tot].dis=z; 15 edge[tot].next=Head[x]; 16 Head[x]=tot; 17 } 18 int front[maxn]; 19 int dis[25][maxn],vis[maxn];//用于对每个点跑dijkstra 20 struct Edge{ 21 int dis,id; 22 Edge(int x,int y){ 23 id=x;dis=y; 24 } 25 bool operator < (const Edge &a)const{ 26 return a.dis<dis; 27 } 28 }; 29 priority_queue q; 30 void dijkstra(int x){ //从x点出发的最短路 31 memset(dis[x],0x3f,sizeof(dis[x])); 32 memset(vis,0,sizeof(vis)); 33 dis[x][x]=0; 34 q.push(Edge(x,0)); 35 while(!q.empty()){ 36 Edge top=q.top();q.pop(); 37 if(vis[top.id]) continue; 38 vis[top.id]=1; 39 int u=top.id; 40 for(int i=Head[u];i;i=edge[i].next){ 41 int v=edge[i].to; 42 if(dis[x][v]>dis[x][u]+edge[i].dis){ 43 dis[x][v]=dis[x][u]+edge[i].dis; 44 q.push(Edge(v,dis[x][v])); 45 } 46 } 47 } 48 } 49 int dp[25][N]; 50 int main(){ 51 //freopen("a.in","r",stdin); 52 int n,m,k; 53 scanf("%d%d%d",&n,&m,&k); 54 for(int i=1;i<=m;++i){ 55 int x,y,z; 56 scanf("%d%d%d",&x,&y,&z); 57 Add(x,y,z);Add(y,x,z); 58 } 59 int t; 60 scanf("%d",&t); 61 for(int i=1;i<=t;++i){ 62 int x,y; 63 scanf("%d%d",&x,&y); 64 front[y]|=(1<<(x-2)); 65 } 66 int ed=(1<<(k))-1; 67 for(int i=1;i<=k+1;++i) dijkstra(i); 68 memset(dp,-1,sizeof(dp)); 69 dp[1][0]=0; 70 for(int j=0;j<=ed;++j) //DP过程 71 for(int u=1;u<=k+1;++u){ 72 if(dp[u][j]==-1) continue; 73 for(int v=2;v<=k+1;++v){ 74 if(u==v) continue; 75 int now=(j|(1<<(v-2))); 76 if((j&front[v])!=front[v]) continue; 77 if(dp[v][j|(1<<(v-2))]==-1) dp[v][j|(1<<(v-2))]=dp[u][j]+dis[u][v]; 78 else dp[v][j|(1<<(v-2))]=min(dp[v][j|(1<<(v-2))],dp[u][j]+dis[u][v]); 79 //printf("%d\n",dp[u][j|(1<<(u-1))]); 80 } 81 } 82 int ans=0x3f3f3f3f; 83 for(int i=1;i<=k+1;++i) //寻找答案 84 if(dp[i][ed]!=-1) ans=min(ans,dp[i][ed]+dis[i][n]); 85 printf("%d\n",ans); 86 return 0; 87 }
emm....什么时候玩个游戏都会这么累了。
分析: 这道题位于状压DP的复习专题之内,该用什么方法就不用我说了吧。
首先我们要先知道我们只需要知道两只猪的位置就可以求出抛物线的解析式,但是如何求的式子需要我们自己推出,我们根据y1=a*x1*x1+b*x1和y2=a*x2*x2+b*x2可以推知a=(x2*y1-x1*y2)/(x1*x2*(x1-x2)),b=(x1*x1*y2-x2*x2*y1)/(x1*x2*(x1-x2));那么我们在DP开始的准备阶段就可以预处理出每个抛物线可以达到几只猪,在固定一个抛物线后就可以不考虑已经死掉的猪,再去寻找其他可以打到活着的猪的抛物线(代码中vis的作用所在),如此处理出相应状态,最后来写DP:dp[i|part[j]]=min(dp[i|part[j]],dp[i]+1)(part数组记录状态),如此就解决了。
代码:
1 #include2 #include 3 #include 4 #include 5 using namespace std; 6 const int N=1e6+10; 7 struct Pig{ 8 double x,y; 9 }pig[N]; 10 double Abs(double a){ //方便double使用 11 return a < 0 ? -a : a; 12 } 13 void cal(double &a,double &b,double x1,double y1,double x2,double y2){ //计算a,b 14 a=(x2*y1-x1*y2)/(x1*x2*(x1-x2)); 15 b=(x1*x1*y2-x2*x2*y1)/(x1*x2*(x1-x2)); 16 } 17 bool judge(int i,double a,double b){ //判断a*x*x+b*y的抛物线是否经过第i只猪 18 double x=pig[i].x; 19 double y=a*x*x+b*x; 20 if(Abs(y-pig[i].y)<=1e-6) return true;//注意精度 21 return false; 22 } 23 int part[N],cnt; 24 int dp[N]; 25 void Solve(){ 26 memset(part,0,sizeof(part)); 27 cnt=0; 28 int n,m; 29 scanf("%d%d",&n,&m); 30 for(int i=1;i<=n;++i) 31 scanf("%lf%lf",&pig[i].x,&pig[i].y); 32 for(int i=1;i<=n;++i){ 33 part[cnt++]=(1<<(i-1));//处理只打一只猪的情况,应对比如猪在弹弓上方不能一鸟二猪的情况 34 for(int j=i+1,vis=0;j<=n;++j){ //vis作用上文以提到,已经死的猪不再去打 35 if(vis&(1<<(j-1))) continue; 36 double a,b; 37 cal(a,b,pig[i].x,pig[i].y,pig[j].x,pig[j].y); 38 if(a>=0) continue; 39 part[cnt]=(1<<(i-1)); 40 for(int k=j;k<=n;++k) 41 if(judge(k,a,b)){ 42 vis|=(1<<(k-1)); 43 part[cnt]|=(1<<(k-1)); 44 } 45 cnt++; 46 } 47 } 48 memset(dp,0x3f,sizeof(dp)); 49 dp[0]=0; 50 int ed=(1<<(n))-1; 51 for(int i=0;i<=ed;++i) //DP开始 52 for(int j=0;j j){ 53 dp[i|part[j]]=min(dp[i|part[j]],dp[i]+1); 54 } 55 printf("%d\n",dp[ed]); 56 } 57 int main(){ 58 //freopen("a.in","r",stdin); 59 int T; 60 scanf("%d",&T); 61 while(T--) Solve(); 62 return 0; 63 }
在刚刚学tarjan的时候这道题曾经卡了我好一阵,当然,这次它也不遗余力地卡住了我。
分析: 其实这道题本身并不难,只是在进行计数的过程之中的处理有一些麻烦,我们首先考虑,如果一个点不是割点,那么它的存在与否不会影响到其他点的联通情况,答案数为2*(n-1),若他是割点,那么删去它后原图会碎碎地分为好几块,此时的答案因为这几个联通块的大小分别乘起来的总和,但我们显然不会对于每一个割点都去重新求一遍每个块的大小,我们想在tarjan的时候,一个割点所连接的不就是它的所有子树和除此之外的其他点吗,于是我们可以把计算过程简化为对于一个大小为x的子树,它对答案的贡献就是x*(n-x),如果一个点确认是割点的话最后答案再加上(n-sum-1)*(sum+1)+(n-1)就可以了,其中sum表示子树大小总和,sum+1就是连带割点的大小,最后只需在加上一个n-1就行了,即割点到其他点的答案贡献,其他点到割点的在x*(n-x)时已经加上了,当然,根节点我们不能在一开始确认它是不是割点,这一类点可以先当割点处理,如果不是再改为2*(n-1)就行了
代码:
1 #include2 #include 3 #include 4 const int N=1e6+10; 5 typedef long long ll; 6 using namespace std; 7 int n,m,Head[N],tot; 8 struct Node{ 9 int next,to; 10 }edge[N]; 11 void Add(int x,int y){ 12 edge[++tot].to=y; 13 edge[tot].next=Head[x]; 14 Head[x]=tot; 15 } 16 int dfn[N],low[N],siz[N],cut[N]; 17 ll ans[N]; 18 void tarjan(int u){ //求割点传统板子 19 dfn[u]=low[u]=++tot; 20 siz[u]++; 21 ll sum=0; 22 int flag=0; 23 for(int i=Head[u];i;i=edge[i].next){ 24 int v=edge[i].to; 25 if(!dfn[v]){ 26 tarjan(v); 27 siz[u]+=siz[v]; 28 low[u]=min(low[u],low[v]); 29 if(low[v]>=dfn[u]){ 30 ans[u]+=(ll)1*siz[v]*(n-siz[v]); //每个子树的贡献 31 sum+=siz[v]; 32 flag++; 33 if(u!=1||flag>1) cut[u]=1; 34 } 35 } 36 else low[u]=min(low[u],dfn[v]); 37 } 38 if(!cut[u]) ans[u]=2*(n-1); //假割点修改贡献 39 else ans[u]+=(ll)1*(n-sum-1)*(sum+1)+(n-1); 40 } 41 int main(){ 42 scanf("%d%d",&n,&m); 43 for(int i=1;i<=m;++i){ 44 int x,y; 45 scanf("%d%d",&x,&y); 46 Add(x,y);Add(y,x); 47 } 48 tarjan(1); 49 for(int i=1;i<=n;++i) printf("%lld\n",ans[i]);//注意开longlong 50 return 0; 51 }
桥豆麻袋,让我再补充一题
让我先哔哔一下,这道题其实是晚上不想颓,今天tarjan又考的不好,才突发奇想去洛谷上找的一道紫题,其实之前我紫题也做过不少,但是不看题解就能拿到80分的这还是头一道(超时了两个点),虽然讨论区的神犇们都说这是一道水的不能再水的水题,但是现在我内心的激动难以言表呀。(来自一个究极蒟蒻的自我欢喜)
好吧,如果你想看正解,我肯定没有这位大佬说得好https://www.luogu.com.cn/blog/Yeasion/solution-p2403
但是我是想来说一下我的思路。
读完题目后可以很容易地想到,这道题的解决顺序是这样的:建图(难点)->tarjan缩点->拓扑排序求出最长路径。后面两步十分好写,只要你能调好,于是我们的重点压在了建图上,正解的方法是排序,蒟蒻的我当然没有想到,我的想法是,第一遍输入不考虑建边,而是把它的横,纵坐标放在两个向量之中,并用一个map来记录对应坐标节点的门的种类和编号,因为硬开二维数组肯定开不下,n最大是1e5嘛,之后再枚举一遍输入的数字,如果是横,纵的门,分别从vector之中取出他可以到达的边,在map之中取出相应的坐标,如果是任意门,我也没有办法,暴力建边吧,因为水平不好比较麻烦,第一次写的时候还把横纵坐标弄反了,所以才会出现代码中的swap,最后的拓扑排序dp[i]表示从i到出度为0的点的最小距离,最后就是调代码了,之后恭喜你得到了80~90分(开O2)
另外,题目中说的文件输入输出是骗人的,还是标准输入输出。
代码:
1 #include2 #include 3 #include 4 #include 5 #include
总结: 其实今天一天的收获还是很大的,上午虽然考得不好,但是毕竟发现了自己的不足,在下午时努力复习都改完了,今天我感觉我一天的效率还是挺高的,改题时也基本是盲打,晚上做了一道让自己满意的紫题(已根据正解改正思路),希望这种状况可以延续下去,直到集训结束吧。
我没有咕咕咕0.0