multiset 第一次用,太神奇了
题意:push a 把a压栈;pop b 把小于等于b的所有元素中最大的元素的出栈,若没有则输出“No Element!”!
View Code
#include<iostream> #include<stdio.h> #include<set> using namespace std; int main() { multiset<int> st; multiset<int>::iterator it; int n; char op[5]; int tp; while(scanf("%d",&n)!=EOF) { st.clear(); while(n--) { scanf("%s%d",op,&tp); if(op[1]=='u') st.insert(tp); else { it=st.upper_bound(tp);//返回容器元素中大于tp的迭代器 if(it==st.begin()) //等于begin() 表示不存在小于等于tp的元素 printf("No Element!\n"); else { it--; if(*it<=tp) { printf("%d\n",*it); st.erase(it); } else printf("No Element!\n"); } } } printf("\n"); } }
题意:题目的意思很清晰,对于一个有向图,将N个点划分成最少的集合个数,同时满足俩个条件:
1) 任意俩点,若互相可达,则必须在同一个集合中
2)属于同一个集合的任意俩个点对(u,v),至少存在一条路径,使得v对于u 可达 或者 u 对于v 可达
分析:对于上述俩个条件,为了简化问题,需要进行缩点,属于同一个强连通分量的点,缩成同一个点,重新构图,可以用tarjan 算法;
这样,第一个条件就一定满足了,接着只剩下第二个条件了,其实,任意俩点,只要在同一条有向路径上,则可以属于一个集合,,,那么问题就转化为用最小的有向路径去覆盖所有的点(最小路径覆盖数==点数-最大匹配数)
View Code
#include<iostream> #include<algorithm> #include<vector> #include<stack> using namespace std; const int N = 5000+10; vector<int> g[N],g2[N]; stack<int> st; int n,dfn[N],low[N],f[N],index; int num,match[N]; bool vis[N],instack[N]; void tarjan(int u)//求强连通分支 { int v; dfn[u] = low[u] = index++; st.push(u); instack[u] = true; vis[u] = true; for(int i=0; i<g[u].size(); i++) { v = g[u][i]; if(!vis[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if(instack[v]) low[u] = min(low[u], dfn[v]); } if(dfn[u] == low[u]) { do { v = st.top(); instack[v] = false; st.pop(); f[v]=num;//记录每一个点所在的强连通分支 } while(v != u); num++; } } int path(int s) { vector<int>::iterator it=g2[s].begin(); for(;it!=g2[s].end();it++) { int v=*it; if(!vis[v]) { vis[v]=true; if(match[v]==-1 || path(match[v])) { match[v]=s; return 1; } } } return 0; } int main() { int T,cas=0,m,a,b; scanf("%d",&T); while(T--) { scanf("%d %d",&n,&m); for(int i=0;i<=n;i++) { g[i].clear(); g2[i].clear(); } while(m--) { scanf("%d %d",&a,&b); g[a].push_back(b); } memset(vis,false,sizeof(vis)); memset(instack,false,sizeof(instack)); index=num=0; for(int i=1;i<=n;i++) { if(!vis[i]) tarjan(i); } for(int i=1;i<=n;i++) //重新构图 for(int j=0;j<g[i].size();j++) { if(f[i]!=f[g[i][j]]) { g2[f[i]].push_back(f[g[i][j]]); } } memset(match,-1,sizeof(match)); int ans=0; for(int i=0;i<num;i++)//求最大匹配数 { memset(vis,false,sizeof(vis)); ans+=path(i); } printf("%d\n",num-ans); } return 0; }
题意: 给定一个有向图,有路径(边)权值和节点的权值,求一个字典序最小的最短路径(中间节点权值+路径权值)
分析:此题目需要对floyd算法有比较深的了解,首先floyd是一个不断拓展路径的过程,同时也是不断增加中间节点的过程,所以累加中间节点权值的部分很好处理。
理解了floyd 算法的过程之后,记录路径也非难事,关键是题目要求的是字典序最小的最短路径。
记录路径有俩种方式:
1)
path[i][j] 记录 起点为i ,终点为j 的路径上j 的直接前驱
View Code
void init(int n) { for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { scanf("%d",&g[i][j]); if(i!=j && g[i][j]!=-1) path[i][j]=i; else path[i][j]=-1; } for(int i=1;i<=n;i++) scanf("%d",&b[i]); } void floyd(int n) { for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) { if(i==k || g[i][k]==-1) continue; for(int j=1;j<=n;j++) { if(i==j || j==k) continue; if(g[k][j]==-1) continue; int newdis=g[i][k]+g[k][j]+b[k];//k为中间节点 if(g[i][j]==-1 || g[i][j]>newdis) { g[i][j]=newdis; path[i][j]=path[k][j]; } else if(g[i][j]==newdis && path[i][j]>path[k][j]) path[i][j]=path[k][j]; } } }
事实上,上面这种方法并不能保证字典序,但不能否定的是,这是一种记录路径的方法
为什么不能保证字典序呢?看一下更新路径的代码:
if(g[i][j]==newdis && path[i][j]>path[k][j])
path[i][j]=path[k][j];
更新的是j的直接前驱,其实也很明显了,单纯比较j 的直接并不能保证字典序的大小。。因为i的直接后继才是关键
2)
path[i][j] 记录起点为i ,中终点为j 的路径上i 的直接后继
View Code
inline void init(int n) { for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { scanf("%d",&g[i][j]); path[i][j]=j; } for(int i=1;i<=n;i++) scanf("%d",&b[i]); } inline void floyd(int n) { for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) { if(i==k || g[i][k]==-1) continue; for(int j=1;j<=n;j++) { if(i==j || j==k) continue; if(g[k][j]==-1) continue; int newdis=g[i][k]+g[k][j]+b[k]; if(g[i][j]==-1 || g[i][j]>newdis) { g[i][j]=newdis; path[i][j]=path[i][k]; } else if(g[i][j]==newdis && path[i][j]>path[i][k]) path[i][j]=path[i][k]; } } }
这种方法就保证了字典序,因为每次拓展路径的时候都是保证起点的直接后继字典序,这样的路径拼接的时候才能保证字典序最小
看完整的代码吧,还有要注意的一点是,数据中有可能出现起点等于终点的情况
View Code
#include<iostream> #include<algorithm> #include<stack> using namespace std; const int N = 50+10; int path[N][N],g[N][N]; int b[N]; inline void init(int n) { for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { scanf("%d",&g[i][j]); path[i][j]=j; } for(int i=1;i<=n;i++) scanf("%d",&b[i]); } inline void floyd(int n) { for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) { if(i==k || g[i][k]==-1) continue; for(int j=1;j<=n;j++) { if(i==j || j==k) continue; if(g[k][j]==-1) continue; int newdis=g[i][k]+g[k][j]+b[k]; if(g[i][j]==-1 || g[i][j]>newdis) { g[i][j]=newdis; path[i][j]=path[i][k]; } else if(g[i][j]==newdis && path[i][j]>path[i][k]) path[i][j]=path[i][k]; } } } inline void printpath(int s,int t) { printf("Path: %d",s); while(path[s][t]!=t) { printf("-->%d",path[s][t]); s=path[s][t]; } printf("-->%d\n",t); } int main() { int n,s,t; while(scanf("%d",&n)==1 && n) { init(n); floyd(n); while(scanf("%d %d",&s,&t)==2) { if(s==-1 && t==-1) break; printf("From %d to %d :\n",s,t); if(s==t) { printf("Path: %d\n",s); printf("Total cost : 0\n\n"); continue; } printpath(s,t); printf("Total cost : %d\n\n",g[s][t]); } } return 0; }
经典的树形游戏的博弈
题目给定一棵树,虽然是规定给定一棵根为1树,但数据给的是树枝,所以是无向的,坑爹啊
View Code
#include<iostream> #include<algorithm> #include<string> #include<vector> using namespace std; const int N = 100000+10; vector<int> g[N]; bool vis[N]; int w[N]; void dfs(int u) { vis[u]=true; int size=g[u].size(); w[u]=0; for(int i=0;i<size;i++) { int v=g[u][i]; if(vis[v]) continue; dfs(v); w[u]^=(w[v]+1); } } int main() { int T,n; int a,b; scanf("%d",&T); while(T--) { scanf("%d",&n); for(int i=0;i<=n;i++) g[i].clear(); for(int i=1;i<n;i++) { scanf("%d %d",&a,&b); g[a].push_back(b); g[b].push_back(a); } memset(vis,false,sizeof(vis)); dfs(1); if(w[1]) puts("Alice"); else puts("Bob"); } return 0; }
题意很浅显,给定当前下载任务的已选状态和最终状态,可以当过单选,全选,反选等三个操作达成,问最少的操作数
分析:思维真的有很大的局限性,怎么就没想到去整理一下规律呢,一心只想着搜索,dp呀,虽然这俩方面都很水……
看了大牛的解释,我也跟着恍然大悟了……
View Code
#include <stdio.h> #include <stdlib.h> #include <string.h> int main() { char S[55],T[55]; int i,j; int Min,count[4],n; while (scanf("%d",&n)!=EOF) { memset(count,0,sizeof(count)); scanf("%s",S); scanf("%s",T); for (i = 0; i < n; i++) { if (S[i]!=T[i]) count[0]++; //记录不同的位,对直接改变这些位需要的操作 else count[1]++; //记录相同的位,反选一次后改变这些位 if(T[i]=='0') count[2]++; //目标序列中0的位,就是不需要选择的TV,全选后复原这些位需要的操作 } //count[0]就是直接点击选择需要的总操作次数 count[1]++; //count[1]为反选一次后再作选择总操作,增加一次反选操作 count[2]++; //count[2]是全选一次后取消选择总操作,count[2]为全选后取消选择需要的操作,增加一次全选操作 count[3] = 2+n-(count[2]-1); //n-(count[2]-1)等于需要选择看的TV总数 ,再+2表示全选反选归零后重新选择的做法 //count[0]直接选择是理所当然的做法。 //其实关键是需要考虑到,对于全选和反选,都是只是做一次的,或者各做一次。 //这个很容易可以证明,因为两次反选后对每一位来说,状态又回到原来的去,相当于多花费两次操作。 //每次全选就等于回到到第一次的全选了。 //全选之后反选归零。 //其实联想到逻辑代数里的一些关系,还是挺直观的。。。为什么当时就没想到!!!! Min = count[0]; for (i = 1; i < 4; i++) Min = (count[i]<Min?count[i]:Min); printf("%d\n",Min); } return 0; }


