2017 清北济南考前刷题Day 4 morning
考场思路:
倒着算就是
可以对一个数-1
可以合并两个数
可以证明只有0和0才能执行合并操作
然后模拟
#include<cstdio> #include<iostream> #include<algorithm> using namespace std; #define N 1000001 void read(int &x) { x=0; char c=getchar(); while(!isdigit(c)) c=getchar(); while(isdigit(c)) { x=x*10+c-'0'; c=getchar(); } } int a[N]; int main() { //freopen("multiset.in","r",stdin); // freopen("multiset.out","w",stdout); int n; read(n); int sum0=0,cnt=0,x; for(int i=1;i<=n;i++) { read(x); if(!x) sum0++; else a[++cnt]=x; } sort(a+1,a+cnt+1); long long ans=0,gather=0; for(int i=1;i<=cnt;i++) { if(!a[i]) break; a[i]-=gather; x=a[i]; gather+=x; ans+=x; while(x) { x--; if(sum0>1) sum0=sum0+1>>1; else break; } sum0++; while(i<cnt && a[i+1]-gather==0) { sum0++; a[++i]-=gather; } } while(sum0>1) ans++,sum0=sum0+1>>1; cout<<ans; }
考场上没注意有向图。。。。
一条道路如果能在上一组,那么肯定把它放在上一组最优
所以可以没加一条边,就判断当前1和n是否联通
判断方式: u-->v若现在u没有与1联通,就不管他
若u和v都与1联通了,那也不管他
若 u与1联通,而v 没有联通,那就再从v开始bfs
这样 每条边只会被访问一次
#include<queue> #include<cstdio> #include<cstring> #include<iostream> using namespace std; #define N 200001 #define M 500001 int n; int front[N],nxt[M],to[M],tot; bool vis[N]; int use[N],cnt; queue<int>q; void read(int &x) { x=0; char c=getchar(); while(!isdigit(c)) c=getchar(); while(isdigit(c)) { x=x*10+c-'0'; c=getchar(); } } void add(int u,int v) { to[++tot]=v; nxt[tot]=front[u]; front[u]=tot; } bool bfs(int s) { if(s==n) return true; while(!q.empty()) q.pop(); q.push(s); int now; while(!q.empty()) { now=q.front(); q.pop(); for(int i=front[now];i;i=nxt[i]) if(!vis[to[i]]) { use[++cnt]=to[i]; if(to[i]==n) return true; vis[to[i]]=true; q.push(to[i]); } } return false; } int main() { freopen("road.in","r",stdin); freopen("road.out","w",stdout); int m; read(n); read(m); int u,v; int ans=1; vis[1]=true; for(int i=1;i<=m;++i) { read(u); read(v); if(vis[u] && !vis[v]) { add(u,v); vis[v]=true; use[++cnt]=v; if(bfs(v)) { // printf("%d\n",i); for(int i=1;i<=cnt;++i) vis[use[i]]=false,front[use[i]]=0; front[1]=0; cnt=tot=0; ans++; add(u,v); if(u!=1) use[++cnt]=u; if(u==1) vis[v]=true,use[++cnt]=v; } } else if(!(vis[u] && vis[v])) add(u,v),use[++cnt]=u; } cout<<ans; }
std思路:
结合了倍增的二分
如果用朴素的二分,会被m条边分m组卡成mm
先考虑1条边 能否使其联通,不能再考虑2条边,然后4条,8条……
若在2^p时 不能联通了,那么在2^p-1 ~ 2^p 范围内二分
这样时间复杂度是mlogm的
如果小兵的血量是1 2 3 4 5 ……
那么显然我们可以补到所有的兵
如果有相同血量的兵,那么只能补到其中的1个兵
所以我们要尽可能的把给出的兵的血量变成1 2 3 4 5 ……
一种可行的方案是 重复血量的兵 强制消耗代价使他 变成 血量更小 但血量不重复的兵
可以用栈记录之前没有的血量,每遇到一个重复的就用栈中的一个血量
例:1 2 4 4
扫到4的时候,之前没有血量3,3入栈
后面还是1个4,就让3出栈,即消耗代价 使4变成3
令c[i]=j 记录消耗代价后血量为i的兵实际上是原血量为j的兵
然后DP
dp[i][j] 表示到血量为i的兵,省下了j刀的最大补兵数
省下了j刀:就是先把兵的血量看成1,2,3,然后考虑每个兵砍或不砍。如果不砍,就可以省下一刀给以后用
所以如果不砍,状态转移为 dp[i][j]=dp[i-1][j-1]
如果砍的话,砍血量为i的兵要加上之前强制消耗的代价,所以dp[i][j]=dp[i-1][j+c[i]-i]+1
老鹿的攻击怎么体现的呢?
因为每次尽可能的让兵的血量变为1,2,3……
自己砍掉一个血量为1的兵,后面再老鹿的攻击下又产生了一个血量为1的兵
但实际DP时
从1开始枚举血量
血量为2时,实际血量为1,相当于 提高了 兵死亡时的血量
#include<cstdio> #include<cstring> #include<algorithm> #include<iostream> using namespace std; #define N 1001 int f[N][N]; int a[N],c[N]; int cnt[N]; int st[N],top; int main() { freopen("cs.in","r",stdin); freopen("cs.out","w",stdout); int T,n,mx; scanf("%d",&T); while(T--) { memset(f,0,sizeof(f)); memset(cnt,0,sizeof(cnt)); memset(c,0,sizeof(c)); top=mx=0; scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",&a[i]),mx=max(mx,a[i]),cnt[a[i]]++; sort(a+1,a+n+1); for(int i=1;i<=mx;i++) if(!cnt[i]) st[++top]=i; else { while(cnt[i]>1 && top ) c[st[top--]]=i,--cnt[i]; c[i]=i; } int ans=0; for(int i=1;i<=mx;++i) for(int j=0;j<i;++j) { if(j) f[i][j]=f[i-1][j-1]; if(c[i] && j+c[i]-i<i) f[i][j]=max(f[i][j],f[i-1][j+c[i]-i]+1); ans=max(ans,f[i][j]); } cout<<ans<<'\n'; } }