这种题目详解,是“一日一测”与“一句话题解”栏目所无法覆盖的,可能是考试用题,也可能是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,同志们可以看一看):

Some Interesting Problems(持续更新中)

Some Interesting Problems(持续更新中)

 大概就是这样了。最后说一下,建议使用读入优化,不然结果会很让人伤心。

还有,对了,做完了可以看一看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 }
IRREGULAR

相关文章: