这种题目详解,是“一日一测”与“一句话题解”栏目所无法覆盖的,可能是考试用题,也可能是OJ题目。常常非常经典,可以见微知著。故选其精华,小列如下。
T1:fleet 给定一个序列,询问[L,R]间有多少种不同的权值。(普通数据结构范围)
e.g.:序列1,1,2,3,2的[1,5]有3种不同权值,[1,3]有2种不同权值。
ANSWER:可以考虑使用主席树求解。查询[L,R]时返回root[R]的[L,R]值之和。root[i]与root[i-1]的不同在于:prev[aa[i]]这个位置(即上一次出现aa[i]的位置)-1,在i这个位置+1。先预处理完毕,再应付查询。
T2:给定一个序列,询问[L,R]间有多少种权值k恰出现k次。(普通数据结构范围)
e.g.:序列1,1,2,3,2的[1,1]有1种权值,[1,2]有0种权值,[2,5]有2种权值。
ANSWER:可以考虑使用主席树求解。但是,我们也可以考虑使用离线。因为答案本质上统计的是一种情形,而我们可以考虑该情形对答案的贡献。如果把询问[L,R]转化为二维矩阵中某点[L,R]的权值,那么每一种情形的贡献也可以看做是矩形的修改。因为这样很令人不爽,可以进一步地转换,使用差分的思想,将矩形拆成4个角,问题于是转化为单点修改与矩阵前缀和的查询。这个东西很像BZOJ的一道题:MONICA。那是一道CDQ分治套树状数组的典型题目,因为有时间的先后。但是,这道题目完全可以先修改再查询,于是可以sort以后直接使用树状数组求解。然而,如果你真的能够看懂上面那段东西,你可能就会惊奇的发现,这种思路其实和主席树一模一样。于是,我们可以反观我们的学习过程。当我们在学主席树的时候,真的有想过离线的做法吗?
1 #define PN "count" 2 #include <cstdio> 3 #include <cstring> 4 #include <algorithm> 5 template<class T>inline void readint(T &res) { 6 static char ch;T flag=1; 7 while((ch=getchar())<'0'||ch>'9')if(ch=='-')flag=-1; 8 res=ch-48; 9 while((ch=getchar())>='0'&&ch<='9')res=res*10+ch-48; 10 res*=flag; 11 } 12 const int N = 1000000 + 100; 13 const int Q = 1000000 + 100; 14 struct DATUM { 15 int x, y, delta; 16 bool operator<(const DATUM &rhs) const { 17 if(x!=rhs.x) return x<rhs.x; 18 if(y!=rhs.y) return y<rhs.y; 19 if(delta!=rhs.delta) return delta<rhs.delta; 20 } 21 } data[N*4+Q]; 22 int tot, ans[Q]; 23 inline void addD(int x,int y,int delta) {data[++tot]=(DATUM){x,y,delta};} 24 25 int n, a[N]; 26 void add(int pos,int val) {for(int x=pos;x<=n;x+=x&-x)a[x]+=val;} 27 int query(int pos) {int val=0;for(int x=pos;x;x-=x&-x)val+=a[x];return val;} 28 29 #include <vector> 30 std::vector<int> same[N]; 31 int main() { 32 int q;readint(n);readint(q); 33 for( int i = 1; i <= n; i++ ) same[i].push_back(0); 34 for( int i = 1, x, siz; i <= n; i++ ) { 35 readint(x);same[x].push_back(i); 36 siz=same[x].size(); 37 if(siz>x) { 38 addD(same[x][siz-x-1]+1,i,+1); 39 addD(same[x][siz-x]+1,i,-1); 40 if(siz>x+1) addD(same[x][siz-x-2]+1,i,-1); 41 if(siz>x+1) addD(same[x][siz-x-1]+1,i,+1); 42 } 43 } 44 for( int i = 1, l, r; i <= q; i++ ) readint(l),readint(r),addD(l,r,i+1); 45 std::sort(data+1,data+tot+1); 46 for( int i = 1; i <= tot; i++ ) { 47 if(data[i].delta<=1) add(data[i].y,data[i].delta); 48 else if(data[i].y>=1&&data[i].y<=n) ans[data[i].delta-1]=query(data[i].y); 49 } 50 for( int i = 1; i <= q; i++ ) printf("%d\n",ans[i]); 51 return 0; 52 }
T3:给定n个长度为m的字符串,问有多少对字符串的相同字符数为0,为1,为2,为3,……为m。输出m+1个数。(总字符数≤1e5)
e.g.:4个长度为3的字符串xyz,xyz,zzx,xzz中有2对字符串的相同字符数为0,有1对字符串的相同字符数为1,有2对字符串的相同字符数为2,有1对字符串的相同字符数为3。
ANSWER:首先考虑暴力,是稳定的n^2*m,需要满足n约小于1000。然后,请忽略数据有意形成的数据断带。之后,对于m很小的情况,可以考虑暴力枚举出所有的字符串(就是状态压缩),因为相同字符数为m的情况可以直接预知,而又可以枚举每一位局部不同然后加以统计。于是考虑DP[2][S][j],表示在修改第i位时,与串S的相同字符数为j的串的数目。最后简单修改后,就可以输出答案。代码如下:
1 #define PN "words" 2 #include <cstdio> 3 #include <cstring> 4 #include <algorithm> 5 6 const int N = 100000 + 1000; 7 int n, m; 8 long long bucket[N]; 9 10 char s[N]; 11 #define HOLE(x,y) s[(x-1)*m+y] 12 void bruce() { 13 for( int i = 1; i <= n; i++ ) scanf("%s",s+(i-1)*m); 14 for( int i = 1, j, k, tot; i <= n; i++ ) for( j = i + 1; j <= n; j++ ) { 15 tot = 0; 16 for( k = 0; k < m; k++ ) if(HOLE(i,k)==HOLE(j,k)) tot++; 17 bucket[tot]++; 18 } 19 } 20 21 const int APP = 531441; 22 const int M = 13; 23 int appear[APP], cur, three[M]; 24 long long f[2][APP][M]; 25 char ch; 26 void solve() { 27 for( int i = 1, now, j; i <= n; i++ ) { 28 now = 0; 29 for( j = 1; j <= m; j++, now*=3 ) { 30 while((ch=getchar())==' '||ch=='\n');ch-='x'; 31 now+=ch; 32 } 33 appear[now/3]++; 34 } 35 three[0]=1; 36 for( int i = 1; i <= m; i++ ) three[i]=three[i-1]*3; 37 for( int i = 0; i < three[m]; i++ ) f[cur][i][m]=appear[i]; 38 for( int i = 0, j, k; i < m; i++ ) { 39 cur^=1;memcpy(f[cur],f[cur^1],sizeof(f[cur^1])); 40 for( j = 0; j < three[m]; j++ ) { 41 int s1 = (j/three[i]%3)*three[i], s2 = j - s1; 42 for( k = 0; k < three[i+1]; k += three[i]) if(k!=s1) for( int p = m-i; p <= m; p++ ) f[cur][s2+k][p-1]+=f[cur^1][j][p]; 43 } 44 } 45 for( int p = 0, i; p <= m; p++ ) for( i = 0; i < three[m]; i++ ) bucket[p]+=appear[i]*f[cur][i][p]; 46 bucket[m]-=n; 47 for( int i = 0; i <= m; i++ ) bucket[i]/=2; 48 } 49 50 int main() { 51 freopen(PN".in","r",stdin); 52 freopen(PN".out","w",stdout); 53 scanf("%d%d",&n,&m); 54 if(m<=12) solve(); 55 else bruce(); 56 for( int i = 0; i <= m; i++ ) printf("%I64d\n",bucket[i]); 57 return 0; 58 }
T4:BZOJ1001狼抓兔子。求无向方格图的最小割。
ANSWER:这样的最小割↔对偶图的最短路。dinic以前说过,但是对偶图确实可以快上很多倍(实验验证dinic约2500ms,对偶图约400ms)。因为这样的图的最小割一定是能够分隔源点与汇点的一段连续折线。于是可以把边之间的小“胞体”作为节点建边跑最短路。如图(摘自优秀博客http://www.cnblogs.com/jinkun113/p/4682827.html,同志们可以看一看):
大概就是这样了。最后说一下,建议使用读入优化,不然结果会很让人伤心。
还有,对了,做完了可以看一看BZOJ2007[NOI2010]海拔,也是一道类似的题目,不过是有向边。
T5:随rand 给定n个正整数和一个模数mod。还有一个数x其初始值为1,我们将对x进行m次操作。每一次操作是从n个数中随机选出一个数p,然后x=x*p%mod。求x的期望值a/b,为避免输出浮点数请mod1e9+7后输出。(n≤1e5,m≤1e9,mod≤1e3)
e.g.:最开始有2个正整数1和2,mod=3,无论进行多少次操作,x总期望为3/2。
ANSWER:达哥出的题。这道题的暴力转移很好想。因为n和m看上去都很邪恶,而mod却很小,x再猖狂也是在0~mod-1这个范围内的。而最后答案的计算不过是a/b,a就是进行m次操作后每个模数的情况数乘以每个模数求和,而b就是进行m次操作后每个模数的情况数求和。中间“情况数”可以随便mod1e9+7。于是可以建立转移方程了。
方程:dp[i][j]=∑dp[i-1][k]*p[k][j]
在这里,dp[i][j]指进行i次操作后模数j的情况数mod1e9+7。p[k][j]指每一次操作,模数k能有几种转移到模数j的方法。我们可以发现,p[k][j]只与n个数本身有关,是妥妥的常量(特别是n没有用了)。加上滚动,这就是空间复杂度为O(mod^2)而时间复杂度为O(m*mod^2)的DP方法。
但是,m确实很大。考虑到DP方程确实太简单了,我们可以考虑使用矩阵快速幂。于是,空间复杂度仍为O(mod^2),而时间复杂度却成了O(log m*mod^3)。本题的前80%都有mod≤300,似乎80分稳了。在考场上我是这样认为的,但是我最后在老年机上只有20,在极速的floj上也只有50。只要仔细想一想,m最大为1e9,那log m就是30,mod^3=27000000,是绝对过不了的。不过,想到了这里,我们应该放弃吗?绝不!
我们知道O(log m*mod^3)是很慢的,但是O(log m*mod^2)却是可行的。考虑矩阵快速幂的本质,是对顺序的漠视。我们一节base算出来的,在本质上其实是进行若干次操作后1能够变成哪些模数。那么我们其实可以更加直接,因为可以直接乘起来。这样,时间复杂度打了标,空间复杂度亦变成了O(mod)。代码如下。
1 #define PN "rand" 2 #include <cstdio> 3 #include <algorithm> 4 #include <cstring> 5 6 //char *hd, *tl, buf[(1<<15)+2]; 7 //#define getc() ((hd==tl&&(tl=(hd=buf)+fread(buf,1,1<<15,stdin)),hd==tl)?0:*hd++) 8 template<class T>inline void readin(T &res) { 9 static char ch;T flag=1; 10 while((ch=getchar())<'0'||ch>'9')if(ch=='-')flag=-1; 11 res=ch-48; 12 while((ch=getchar())>='0'&&ch<='9')res=res*10+ch-48; 13 res*=flag; 14 } 15 16 const int N = 100000 + 1000; 17 const int SIZ = 1000 + 50; 18 const int MOD = 1e9+7; 19 20 #define set(x) (x>MOD?x-MOD:x) 21 int mod, base[SIZ], rus[SIZ], tmp[SIZ]; 22 23 void calc() { 24 int a = 0, b = 0, ans; 25 for( int pos = 0; pos < mod; pos++ ) a=set(a+(long long)rus[pos]*pos%MOD); 26 for( int pos = 0; pos < mod; pos++ ) b=set(b+rus[pos]); 27 ans = a; 28 for( int bound = MOD - 2; bound; bound>>=1, b=(long long)b*b%MOD ) if(bound&1) ans=(long long)ans*b%MOD; 29 printf("%d\n",ans); 30 } 31 32 void matrix_(int type) { 33 for( int i = 0; i < mod; i++ ) tmp[i]=0; 34 if(!type) { 35 for( int i = 0, j; i < mod; i++ ) for( j = 0; j < mod; j++ ) tmp[i*j%mod]=set(tmp[i*j%mod]+(long long)base[i]*base[j]%MOD); 36 for( int i = 0; i < mod; i++ ) base[i]=tmp[i]; 37 } else { 38 for( int i = 0, j; i < mod; i++ ) for( j = 0; j < mod; j++ ) tmp[i*j%mod]=set(tmp[i*j%mod]+(long long)base[i]*rus[j]%MOD); 39 for( int i = 0; i < mod; i++ ) rus[i]=tmp[i]; 40 } 41 } 42 43 44 int main() { 45 freopen(PN".in","r",stdin); 46 freopen(PN".out","w",stdout); 47 int n, m;readin(n);readin(m);readin(mod); 48 // memset(rus,0,sizeof(rus));memset(base,0,sizeof(base)); 49 for( int i = 1, x, pos; i <= n; i++ ) { 50 readin(x); 51 for( pos = 0; pos < mod; pos++ ) base[x]++; 52 } 53 rus[1]=1; 54 for( int bound = m; bound; bound>>=1, matrix_(0) ) if(bound&1) matrix_(1); 55 calc(); 56 return 0; 57 }
T6:BZOJ 2064 状压DP 给定一个初始集合和目标集合,有两种操作:1.合并集合中的两个元素,新元素为两个元素之和 2.分裂集合中的一个元素,得到的两个新元素之和等于原先的元素。要求用最小步数使初始集合变为目标集合,求最小步数。
ANSWER:当时特别迷,不知道怎么办。后来看了一下题解,实质上是最优子结构的应用。最优子结构操作起来很像套娃。因为最坏情况就是把所有的元素合并成一个,然后再拆开,步数为n+m-2。而我们可以发现,如果一个集合要变成另一个集合,而两个集合都分别存在一个子集之和相等,那么就可以减少2次操作,减少的是一次合并与一次拆分。然后,我们就可以枚举原始集合与目标集合,统计其中可以减少的次数。对于两个给定的集合,我们可以枚举其中的每一个元素,这样就可以间接查询所有子集了。如果两个集合之和相等,那么就可以减少一次操作。最后答案为n+m-2*f[(1<<n)-1][(1<<m)-1]。这是二维的。
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #define smax(x,y) x=std::max(x,y) 5 const int N = 10; 6 int n, m, a[N+1], b[N+1], suma[1<<N], sumb[1<<N], f[1<<N][1<<N]; 7 int main() { 8 scanf("%d",&n); 9 for( int i = 1; i <= n; i++ ) scanf("%d",&a[i]); 10 scanf("%d",&m); 11 for( int i = 1; i <= m; i++ ) scanf("%d",&b[i]); 12 for( int i = 1, pos = 1; pos <= n; i<<=1, pos++ ) suma[i]=a[pos]; 13 for( int j = 1, pos = 1; pos <= m; j<<=1, pos++ ) sumb[j]=b[pos]; 14 for( int i = 1, lima = 1<<n, k = i&-i; i < lima; i++, k = i&-i ) suma[i]=suma[k]+suma[i-k]; 15 for( int j = 1, limb = 1<<m, k = j&-j; j < limb; j++, k = j&-j ) sumb[j]=sumb[k]+sumb[j-k]; 16 for( int i = 1, j, k, lima = 1<<n, limb = 1<<m; i < lima; i++ ) for( j = 1; j < limb; j++ ) { 17 for( k = 1; k <= i; k<<=1 ) if(i&k) smax(f[i][j],f[i-k][j]); 18 for( k = 1; k <= j; k<<=1 ) if(j&k) smax(f[i][j],f[i][j-k]); 19 if(suma[i]==sumb[j]) f[i][j]++; 20 } 21 printf("%d\n",n+m-2*f[(1<<n)-1][(1<<m)-1]); 22 return 0; 23 }
但是,考虑到原始集合与目标集合的实质差异并不大,只是在减少操作时在等号两边,于是可以考虑移项,原始集合是正的,而目标集合是负的,就可以统一考虑了。
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #define smax(x,y) x=std::max(x,y) 5 const int N = 20; 6 int n, m, sum[1<<N], f[1<<N]; 7 int main() { 8 scanf("%d",&n); 9 for( int i = 1, sta = 1; i <= n; i++, sta<<=1 ) scanf("%d",&sum[sta]); 10 scanf("%d",&m); 11 for( int i = 1, x, sta = 1<<n; i <= m; i++, sta<<=1 ) { 12 scanf("%d",&x); 13 sum[sta]=-x; 14 } 15 n+=m; 16 for( int i = 1, lim = 1<<n, k = i&-i, p; i < lim; i++, k = i&-i ) { 17 sum[i]=sum[k]+sum[i-k]; 18 for( p = 1; p <= i; p<<=1 ) if(i&p) smax(f[i],f[i-p]); 19 if(sum[i]==0) f[i]++; 20 } 21 printf("%d\n",n-2*f[(1<<n)-1]); 22 return 0; 23 }
T7:IRREGULAR 给你一棵树,每个点有点权,输出1~n每个点的困难度。一个点P的困难度是指,所有LCA为P的点对的路径异或和的最大值。点对可以是相同的2个点。按DFS序维护可持久化TRIE,为差量全局桶,询问时启发式合并。
E.G:对于一棵5个结点的树,1为根有儿子2、3和5,2和5是叶子结点,3有儿子4。5个点的权值分别为1775、6503、8147、2354和8484,可以算出这5个点的困难度分别为16044、6503、8147、2354和8484。
ANSWER:对于最大异或和这种经典问题,一个很直观的想法就是使用01TRIE。但是,本人当时一直是想使用DFS进行统计,将下面的所有直接合并到上面,显然不可行。于是考场上选择暴力枚举点对写ST表,无奈其他地方时间耗费太多,有东西没考虑到,这道题连暴力分也没有得到。
事后发现了一个性质(其实我以前曾经知道),就是u点到v点的路径异或和=dis[u]^dis[v]^dis[LCA]^aa[LCA],这里dis[u]指从u向上到根的点权异或和,aa[LCA]当然就是LCA的点权啦。
然后根据这个性质可以发现,其实不用从下往上,直接用可持久化TRIE树和DFS序就好了。于是可以先搞出DFS序,再构造出可持久化TRIE,再顺着统计每个结点的答案,因为一棵子树的DFS序是连续的,就可以很方便地统计询问了。
这样似乎是可以的,但是复杂度其实没有办法保证,于是考虑启发式合并。使用重链剖分方便地保证重子树优先,然后就小幷大了。
上面似乎说地很抽象,但最后实施起来其实是很具体的。但是中间有一点小细节还是让我调了很久。一处是query查询[L,R]时减的是L-1,还有一处是insert是siz的处理。
代码如下:
1 #define PN "irregular" 2 #include <cstdio> 3 #include <cstring> 4 #include <algorithm> 5 6 template<class T>inline void readin(T &res) { 7 static char ch;T flag = 1; 8 while((ch=getchar())<'0'||ch>'9')if(ch=='-')flag=-1; 9 res=ch-48; 10 while((ch=getchar())>='0'&&ch<='9')res=res*10+ch-48; 11 res*=flag; 12 } 13 14 const int N = 100000 + 1000; 15 const int ROOT = 1; 16 struct Edge {int v,upre;}g[N<<1]; 17 int head[N], ne = 0; 18 inline void adde(int u,int v) {g[++ne]=(Edge){v,head[u]};head[u]=ne;} 19 20 int n, aa[N]; 21 22 int fa[N], dis[N], s[N], son[N]; 23 void DFS(int u) { 24 dis[u]=dis[fa[u]]^aa[u]; 25 s[u]=1; 26 for( int i = head[u], v; i; i = g[i].upre ) { 27 if((v=g[i].v)==fa[u]) continue; 28 fa[v]=u; 29 DFS(v); 30 s[u]+=s[v]; 31 if(s[son[u]]<s[v]) son[u]=v; 32 } 33 } 34 35 int in[N], out[N], seq[N], idy; 36 void DFS2(int u) { 37 in[u]=++idy;seq[idy]=u; 38 if(son[u]) DFS2(son[u]); 39 for( int i = head[u], v; i; i = g[i].upre ) { 40 if((v=g[i].v)==fa[u]||v==son[u]) continue; 41 DFS2(v); 42 } 43 out[u]=idy; 44 } 45 46 const int BITS = 32; 47 const int NODES = N * 60; 48 int digit[BITS], root[N], siz[NODES], ch[NODES][2], tail; 49 int query(int L,int R,int x) { 50 L = root[L], R = root[R]; 51 for( int floor = 0; floor < BITS; floor++,x>>=1 ) digit[floor]=x&1; 52 int ans = 0; 53 for( int floor = BITS - 1; floor >= 0; floor-- ) { 54 ans<<=1; 55 if(siz[ch[R][digit[floor]^1]]-siz[ch[L][digit[floor]^1]]) { 56 ans++; 57 L = ch[L][digit[floor]^1], R = ch[R][digit[floor]^1]; 58 } else L = ch[L][digit[floor]], R = ch[R][digit[floor]]; 59 } 60 return ans; 61 } 62 63 int main() { 64 freopen(PN".in","r",stdin); 65 freopen(PN".out","w",stdout); 66 readin(n); 67 for( int i = 1; i <= n; i++ ) readin(aa[i]); 68 for( int i = +1, u, v; i < n; i++ ) { 69 readin(u);readin(v); 70 adde(u,v);adde(v,u); 71 } 72 fa[ROOT]=ROOT; 73 DFS(ROOT); 74 DFS2(ROOT); 75 for( int i = 1; i <= n; i++ ) { 76 for( int floor = 0, x = dis[seq[i]]; floor < BITS; floor++,x>>=1 ) digit[floor]=x&1; 77 int ori = root[i-1], now = root[i] = ++tail; 78 for( int floor = BITS - 1; floor >= 0; floor-- ) { 79 siz[now]=siz[ori]+1; 80 ch[now][digit[floor]^1]=ch[ori][digit[floor]^1]; 81 ori = ch[ori][digit[floor]]; 82 now = ch[now][digit[floor]] = ++tail; 83 } 84 siz[now]=siz[ori]+1;//注意此处 85 } 86 for( int u = 1; u <= n; u++ ) { 87 int ans = aa[u]; 88 if(son[u]) ans = std::max(ans,query(in[son[u]]-1,out[son[u]],dis[u]^aa[u]));//注意左端减一 89 for( int v = seq[out[son[u]]+1], x; fa[v]==u; v = seq[out[v]+1] ) { 90 for( x = in[v]; x <= out[v]; x++ ) 91 ans = std::max(ans,query(in[u]-1,in[v]-1,dis[seq[x]]^aa[u])); 92 } 93 printf("%d\n",ans); 94 } 95 return 0; 96 }