A.
题意:求去掉d物品后容量为e最大背包。每个物品有三种属性,权值、容量、数量。
#include <bits/stdc++.h> using namespace std; const int V=1000, N=1005; void zop(int *d, int w, int v) { for(int i=V; i>=v; --i) d[i]=max(d[i], d[i-v]+w); } void cmp(int *d, int w, int v) { for(int i=v; i<=V; ++i) d[i]=max(d[i], d[i-v]+w); } void dcp(int *d, int w, int v, int c) { if(c*v>=V) { cmp(d, w, v); return; } for(int i=1; i<=c; ++i) zop(d, w, v); } int l[N][V+5], r[N][V+5], a[N], b[N], c[N], n; int main() { scanf("%d", &n); for(int i=1; i<=n; ++i) scanf("%d%d%d", &a[i], &b[i], &c[i]); for(int i=1; i<=n; ++i) memcpy(l[i], l[i-1], sizeof l[i-1]), dcp(l[i], b[i], a[i], c[i]); for(int i=n; i>=1; --i) memcpy(r[i], r[i+1], sizeof r[i+1]), dcp(r[i], b[i], a[i], c[i]); int q; scanf("%d", &q); while(q--) { int d, e, ans=0; scanf("%d%d", &d, &e); ++d; for(int i=0; i<=e; ++i) ans=max(ans, l[d-1][i]+r[d+1][e-i]); printf("%d ", ans); } return 0; }
考场sb没话说,只拿了80分暴力分= =
题解:背包其实是可以合并的,因此预处理前面的背包和后面的背包,查询的时候合并即可。(妈呀谁来教我多重背包怎么优化啊,二进制分组貌似并不能计算出每一个背包的值啊= =
B.
题意:对于博弈树,定义最小黑方胜集合为最小的叶子节点集合,满足集合内的叶子如果确定是胜利,那么黑方必胜。最小白方胜集合同理。现在给出博弈树,求黑方先手的情况下既在最小白方胜集合的点也在最小黑方胜集合的点。
#include <bits/stdc++.h> using namespace std; const int N=200005; struct E { int next, to; }e[N<<1]; int ihead[N], cnt, n, sz[N]; void add(int x, int y) { e[++cnt]=(E){ihead[x], y}; ihead[x]=cnt; e[++cnt]=(E){ihead[y], x}; ihead[y]=cnt; } int dfs1(int x, int f, bool dep=0) { int ret=dep?0:~0u>>1, c=1; for(int i=ihead[x]; i; i=e[i].next) if(e[i].to!=f) { c=0; int y=e[i].to; int s=dfs1(y, x, !dep); if(dep) ret+=s; else ret=min(ret, s); } if(c) return sz[x]=1; return sz[x]=ret; } void dfs2(int x, int f, int *ok, bool dep=0) { int c=1; for(int i=ihead[x]; i; i=e[i].next) if(e[i].to!=f) { c=0; int y=e[i].to; if(dep) dfs2(y, x, ok, !dep); else { if(sz[y]==sz[x]) dfs2(y, x, ok, !dep); } } if(c) ok[x]=1; } void work(int *ok, int rt) { dfs1(rt, -1); dfs2(rt, -1, ok); } int vis[2][N]; int main() { scanf("%d", &n); for(int i=2; i<=n; ++i) { int x; scanf("%d", &x); add(x, i); } work(vis[0], 1); add(n+1, 1); work(vis[1], n+1); int ans=0, tot=0, xo=0; for(int i=1; i<=n; ++i) if(vis[0][i] && vis[1][i]) { if(!ans) ans=i; tot++; xo^=i; } printf("%d %d %d ", ans, tot, xo); return 0; }
推了一下结论,首先根据博弈论,当前节点必胜当且仅当后继节点至少有一个必败态,必败当且仅当后继节点全为必胜态。可以发现当前节点要胜时,后继只需要一个必败态,而不可能大于两个必败态(多余两个只会增大集合)。因此按照博弈论的知识对树dfs两次就好辣。求白方必胜我们只需要新加一个节点连边黑方先手的节点就行啦。
C.
题意:在线插入线段or询问x=a的直线与所有线段的交点y最大的线段最小的标号。
#include <bits/stdc++.h> using namespace std; typedef long double lf; const int lim=39989, M=lim<<2; struct S { int id; lf k, b; lf cal(int x) const { return k*x+b; } }t[M]; lf X(S &x, S &y) { return (y.b-x.b)/(x.k-y.k); } void update2(S &a, int l, int r, int x) { if(t[x].id==0) { t[x]=a; return; } if(t[x].cal(l)<a.cal(l)) swap(t[x], a); if(l==r || t[x].k==a.k) return; lf p=X(t[x], a); if(p<l || p>r) return; int mid=(l+r)>>1; if(p<=mid) update2(t[x], l, mid, x<<1), t[x]=a; else update2(a, mid+1, r, x<<1|1); } void update(S a, int L, int R, int l=1, int r=lim, int x=1) { if(L<=l && r<=R) { update2(a, l, r, x); return; } int mid=(l+r)>>1; if(L<=mid) update(a, L, R, l, mid, x<<1); if(mid<R) update(a, L, R, mid+1, r, x<<1|1); } inline S min(const S &a, const S &b, const int &x) { lf y1=a.cal(x), y2=b.cal(x); if(y1==y2) return a.id<b.id?a:b; return y1>y2?a:b; } S query(int p, int l=1, int r=lim, int x=1) { if(l==r) return t[x]; int mid=(l+r)>>1; if(p<=mid) return min(t[x], query(p, l, mid, x<<1), p); else return min(t[x], query(p, mid+1, r, x<<1|1), p); } int last, n; int got(int x, int mo=1000000000) { return (x+last-1)%mo+1; } int main() { int q; scanf("%d", &q); while(q--) { int c; scanf("%d", &c); if(c==0) { int x; scanf("%d", &x); x=got(x, lim); printf("%d ", last=query(x).id); } else { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); x1=got(x1, lim), x2=got(x2, lim); y1=got(y1); y2=got(y2); if(x1>x2) swap(x1, x2), swap(y1, y2); lf k, b; if(x1==x2) k=0, b=max(y1, y2); else k=(lf)(y1-y2)/(x1-x2), b=-k*x1+y1; S a={++n, k, b}; update(a, x1, x2); } } return 0; }
线段树好题。。。。。由于水平弱考场上就敲了个30分暴力= =
维护1~maxX的线段树,线段树每个节点只维护一个线段!(到后面有办法取出答案,一开始我认为如果插入线段可能会要在一个节点内存多段线段= =所以当时认为线段树不可做!可是事实上是可做的!)
对于插入的线段,如果当前节点有线段,那么考虑两种情况:
1、无交点:此时取最高的线段(无论何时这个区间都不会用到另一条线段了)
2、有交点:这里就是关键所在,首先交点肯定在[l, mid]或者(mid, r]的其中一个,当交点在[l, mid]时,我们将当前节点维护的线段改为(mid, r]有碾压另一条线段的线段(即在这一半另一条线段都不可能最优),然后将另一条线段递归插入到[l, mid]这个节点上。当交点在(mid, r]上时,当前节点维护的线段改为[l, mid]碾压另一条线段的线段,然后用另一条线段插入到[mid+1, r]的节点。
最后查询的时候查询所有包含这个点的线段树节点即可。单次查询$O(logn)$
为什么这样做是正确的呢?
我们只需要证明包含这个点x的最优线段一定在插入的时候被插入到了[x, x]叶子节点到根的路径上的某个节点。即这条线段在某次更新中由于[l, mid]或(mid, r]有碾压其它线段的性质被保存了下来。
依次考虑其余线段,假设当前线段与这条最优线段相遇在一个节点A。同上面考虑的情况2,由于递归定义,最终要么插入到了[x, x]的叶子节点(因为有最强的碾压性质),要么在[x, x]-根的某个节点存活了下来。然后就证明完了= =
D.
题意:给出一个序列A,求一个序列B使得max{|B[i]-A[i]|}最小。
#include <bits/stdc++.h> using namespace std; const int N=5000005; typedef long long ll; int a[N], n, Sa, Sb, Sc, Sd, mo; inline int F(int x) { x%=mo; return ((ll)Sa*x%mo*x%mo*x%mo + ((ll)Sb*x%mo*x%mo + ((ll)Sc*x%mo + Sd)%mo)%mo)%mo; } bool check(ll d) { ll pre=(~0ull>>1)+1; for(int i=1; i<=n; ++i) { if((ll)a[i]>=pre) pre=max((ll)a[i]-d, pre); else { if((ll)a[i]+d<pre) return 0; } } return 1; } int main() { scanf("%d%d%d%d%d%d%d", &n, &Sa, &Sb, &Sc, &Sd, &a[1], &mo); Sa%=mo; Sb%=mo; Sc%=mo; Sd%=mo; for(int i=2; i<=n; ++i) a[i]=(F(a[i-1])+F(a[i-2]))%mo; ll l=0, r=(ll)(~0u>>1)*2ll; while(l<=r) { ll mid=(l+r)>>1; if(check(mid)) r=mid-1; else l=mid+1; } printf("%lld ", r+1); return 0; }
一开始看错题以为是sum= =后来发现是max。。。。。。
于是二分答案就行辣= =
然后发现题解是有一个结论= =差值最大的逆序对的差值除以二取上界= =。。。。(好像显然吧= =。。。