NOIP2018 - 暑期博客整理

暑假写的一些博客复习一遍。顺便再写一遍或者以现在的角度补充一点东西。

盛暑七月

初涉基环外向树dp&&bzoj1040: [ZJOI2008]骑士

比较经典的基环外向树dp。可以借鉴的技巧在于将每一个环拆出一条边,使剩下部分成为树。再然后就是max(f[u][0],f[v][0])思考中可能会出现的纰漏。

44     for (i=1; i<=n; i++)
45     {
46         v[i] = read(), tt = read();
47         if (get(tt)!=get(i)){
48             addedge(i, tt);
49             fa[fa[tt]] = fa[i];
50         }else lDes[++cnt] = tt, rDes[cnt] = i;
51     }

小技巧就是这个。

【线段树】bzoj3585: mex

区间问题。这个算是一类套路吧,和【细节题 离线 树状数组】luoguP4919 Marisa采蘑菇以及HH的项链大同小异。无非是先将询问排序;再利用询问区间不断右移来更新答案;最后数据结构来辅助完成更新区间内答案的这么一个操作。

这类问题,感觉相比而言区间带修莫队没什么优势。

【树形dp】bzoj1304: [CQOI2009]叶子的染色

自底向上这么一个处理过程不难想到。这类问题和贪心“反悔”有那么点相像,大致意思是说,先考虑一个节点$u$在它能力范围内完成要求的代价;之后转移时候再来看能不能省去$u$付出的代价。

但如果节点的要求有一定后效性的话,比如说要求每个节点到根的路径上恰好有$a_i$个白点、$b_i$个黑点;或是至少有$a_i,b_i$个白黑点。可能不能dp?

初涉三元环

这种根号阈值分类的题算是一类分块思想吧。

再者是统计答案时候:1.强制顺序连边;2.根据出度大小更换顺序 。我认为这连个都是将去重做到相当熟练和精妙地步的操作。

 1     for (int i=3; i<=n; i++)
 2         for (int j=head[i]; j!=-1; j=nxt[j])
 3         {
 4             int x = edges[j];
 5             if (deg[x] > size)
 6                 for (int t=nxt[j]; t!=-1; t=nxt[t])
 7                 {
 8                     int y = edges[t];
 9                     if (y >= x) continue;
10                     if (s[x].find(y)!=s[x].end())
11                         ans += ...12                 }
13             else
14                 for (int t=head[x]; t!=-1; t=nxt[t])
15                 {
16                     int y = edges[t];
17                     if (s[i].find(y)!=s[i].end())
18                         ans += ...19                 }
20         }

还是记一记,多感受一下吧。

初涉tarjan缩点

这个没什么多说的,大致是结合其他内容应用。比较高端的例如2-sat。

 1 void tarjan(int x)
 2 {
 3     dfn[x] = low[x] = ++tim, stk[++cnt] = x;
 4     for (int i=head[x]; i!=-1; i=nxt[i])
 5     {
 6         int v = edges[i];
 7         if (!dfn[v])
 8             tarjan(v), low[x] = std::min(low[x], low[v]);
 9         else if (!col[v]) low[x] = std::min(low[x], dfn[v]);
10     }
11     if (dfn[x]==low[x]){
12         col[x] = ++cols, size[cols] = 1;
13         for (; stk[cnt]!=x; cnt--)
14             col[stk[cnt]] = cols, size[cols]++;
15         cnt--;
16     }
17 }
18 //一分钟敲的,大概没问题

概述「并查集补集转化」模型&&luoguP1330 封锁阳光大学

一种思想(小技巧)。就是说我的敌人1和敌人2可以合并。姑且称作“补集转化”。

【贪心优化dp决策】bzoj1571: [Usaco2009 Open]滑雪课Ski

dp容易想到,转移也不是太难。主要是dp预处理的技巧。

【贪心】bzoj1572: [Usaco2009 Open]工作安排Job

一类经典的有截止时间的区间贪心。建筑抢修是权值相同、完成时间给定;最大收益是权值不同,而且限定开始时间。

其实仔细考虑之后,不难弄明白这类抽象的替换在不同情景下的意义。 讲到底还是贪心中一块重要的“反悔”操作。

【tarjan 拓扑排序 dp】bzoj1093: [ZJOI2007]最大半连通子图

把题读懂之后就是板子题了,直接码就是。

有个技巧,连通块之间防止重边:

 92         for (int i=head[u]; i!=-1; i=nxt[i])
 93         {
 94             int v = edges[i];
 95             if ((--deg[v])==0) q[++qTail] = v;
 96             if (vis[v]==u) continue;
 97             if (f[v] < f[u]+size[v])
 98                 f[v] = f[u]+size[v], g[v] = g[u];
 99             else if (f[v]==f[u]+size[v])
100                 g[v] = (g[v]+g[u])%p;
101             vis[v] = u;
102         }

然后一个小总结。求最长链若$f[i]$表示以$i$为起点的最长链,则dfs;表示以$i$为终点的最长链。则拓扑。

upd:再打了一遍,然而还是有些小错误。主要是:

  1. 根据缩点后重建图后的边忘记分开存了
  2. 统计方案时候没有判连通块之间的重边
  3. 我也不知道为什么tarjan打成了if (dfn[x]!=low[x])……
  4. maxm开小了……

这样的对于模板的熟悉程度不行啊……

初涉2-SAT

巧用tarjan的一类问题。熟悉tarjan善于建模即可。

【期望 数学】7.6神经衰弱

期望依旧很差……现在看到还是基本没想法。哎,这个离散数学还是不太行。

有一点头绪了。

大致流程还是把期望爆开,拆成概率和权值。

答案下界是$m$没错。那么把牌都翻了一遍之后,最坏情况下是要再翻$m$次把相同牌拿光的。也就是说,在下界的基础上,之后每一次操作的权值是1;而概率是$1-前m次中没抽到相同牌的概率$。

这样理解要稍微方便一些,但实际上我还是不太熟练……

初涉最小表示法&&bzoj1398: Vijos1382寻找主人 Necklace

再码了一遍。zz地把calc()里的变量名字打错了。

 1 #include<bits/stdc++.h>
 2 const int maxn = 1000035;
 3 
 4 bool fl;
 5 int n,sst,tst;
 6 char s[maxn],t[maxn];
 7 
 8 int calc(char *s)
 9 {
10     int i = 0, j = 1, k = 0;
11     while (i < n&&j < n&&k < n)
12     {
13         if (s[(i+k)%n]==s[(j+k)%n]) k++;
14         else{
15             if (s[(i+k)%n] > s[(j+k)%n]) i += k+1;
16             else j += k+1;
17             if (i==j) i++;
18             k = 0;
19         }
20     }
21     return std::min(i, j);
22 }
23 int main()
24 {
25     scanf("%s%s",s,t);
26     fl = 1, n = strlen(s);
27     sst = calc(s), tst = calc(t);
28     for (int i=0; i<n; i++)
29         if (s[(sst+i)%n]!=t[(tst+i)%n]){
30             fl = 0;
31             break;
32         }
33     if (!fl){
34         puts("No");
35         return 0;
36     }else puts("Yes");
37     for (int i=0; i<n; i++)
38         putchar(s[(sst+i)%n]);
39     return 0;
40 }
View Code

【树状数组 离散化】bzoj1573: [Usaco2009 Open]牛绣花cowemb

技巧有两点:

  1. 圆上直线问题的映射,也就是快速判断圆内直线是否相交。
  2. 处理相交但不包含的线段数量。按左端点排序,依次处理,并在处理后加上自己贡献。用一个树状数组维护这个操作就好了。

【meet in middle】poj1840Eqs

meet in middle的板子题,再打了一遍(话说我都忘了poj不能用万能头……)

 1 #include<cstdio>
 2 const int BASE = 25000000;
 3 
 4 short f[BASE+35];
 5 int a,b,c,d,e,ans,g[103];
 6 
 7 int main()
 8 {
 9     scanf("%d%d%d%d%d",&a,&b,&c,&d,&e);
10     for (int i=-50; i<=50; i++) g[i+50] = i*i*i;
11     for (int i=-50; i<=50; i++)
12         if (i) for (int j=-50; j<=50; j++)
13             if (j){
14                 int num = -a*g[i+50]-b*g[j+50];
15                 if (num < 0) num += BASE;
16                 f[num]++;
17             }
18     for (int i=-50; i<=50; i++)
19         if (i) for (int j=-50; j<=50; j++)
20             if (j) for (int k=-50; k<=50; k++)
21                 if (k){
22                     int num = c*g[i+50]+d*g[j+50]+e*g[k+50];
23                     if (num < 0) num += BASE;
24                     ans += f[num];
25                 }
26     printf("%d\n",ans);
27     return 0;
28 }
View Code

遇到这种题如果map太慢,并且存在负数,要记得算好数组空间和偏移量。

【状态压缩 meet in middle】poj3139Balancing the Scale

这里还是一个逐级向上的思想,然后用位运算和状压来辅助这个过程。

要注意的是:

  • a[]要sort,因为next_permutation要从最小的开始才行
  • next_permutation的过程里,需要do-while。虽然很显然但是容易忘。

还不错的meet in middle。

概述「DAG加边至强连通」模型&&luoguP2746校园网Network of Schools

算是一种思考的方式和技巧吧。

【动态规划】poj2353Ministry

看到博客里写的拓扑序……呃,我也不清楚当时怎么想的。这个当然是要考虑一个个推过来的顺序的啊。

不过这个也是要注意一下,有些时候可能打着顺手就直接不管顺序了。

1 for (int j=m-1; j>=1; j--)
2     if (f[i][j] > f[i][j+1]+a[i][j])

其他都还好,算是比较基础的题。再写了一遍也没有写挂。

【动态规划】bzoj2298: [HAOI2011]problem a

经过建模之后就成了带权线段覆盖问题。这个和bzoj1577: [Usaco2009 Feb]庙会捷运Fair Shuttle有些类似。那题是线段可拆开,这题则是线段不能拆。

这题主要是建模的过程比较好,尤其是一步步转化的严谨推论。

【动态规划】luoguP1941 飞扬的小鸟

10.25:晚上试着在基本忘掉题解的情况下1A,打算稳一点过题。所以节奏比较慢,打了一个多点小时,拍了一个半小时。嗯,感觉这种策略还是挺好的。不过这种策略也是要靠足够的思维速度和代码能力支撑起来吧。

不过第一发MLE90pts……算是一个要记得检查变量(特别是调试用的)教训吧。

 1 #include<bits/stdc++.h>
 2 const int maxn = 10035;
 3 const int maxh = 1003;
 4 const int INF = 0x3f3f3f3f;
 5 
 6 int n,m,k,mx,ans;
 7 int up[maxn],dw[maxn],l[maxn],r[maxn];
 8 int f[maxn][maxh],g[maxn][maxh];
 9 bool kick;
10 
11 int read()
12 {
13     char ch = getchar();
14     int num = 0;
15     bool fl = 0;
16     for (; !isdigit(ch); ch=getchar())
17         if (ch=='-') fl = 1;
18     for (; isdigit(ch); ch=getchar())
19         num = (num<<1)+(num<<3)+ch-48;
20     if (fl) num = -num;
21     return num;
22 }
23 void Min(int &x, int y){x = x < y?x:y;}
24 int main()
25 {
26     freopen("uoj17.in","r",stdin);
27     freopen("uoj17.out","w",stdout);
28     memset(f, 0x3f3f3f3f, sizeof f);
29     memset(g, 0x3f3f3f3f, sizeof g);
30     n = read(), m = read(), k = read(), ans = INF;
31     for (int i=0; i<=n; i++) l[i] = 1, r[i] = m;
32     for (int i=0; i<n; i++) up[i] = read(), dw[i] = read();
33     for (int i=1; i<=k; i++)
34     {
35         int p = read();
36         l[p] = read()+1, r[p] = read()-1;
37     }
38     for (int i=1; i<=m; i++) f[0][i] = 0;
39     for (int i=0; i<n; i++)
40     {
41         kick = 0;
42         for (int j=1; j<=m; j++)
43         {
44             if (i&&(g[i][j]!=INF)&&(std::min(j+up[i-1], m) <= r[i]))
45             {
46                 int tov = std::min(j+up[i-1], m);
47                 if (f[i][tov] >= g[i][j]+1){
48                     f[i][tov] = g[i][j]+1;
49                 }
50                 Min(g[i][tov], g[i][j]+1);
51             }            
52         }
53         for (int j=l[i]; j<=r[i]; j++)
54             if (f[i][j]!=INF){
55                 mx = i, kick = 1;
56                 int tov = std::min(j+up[i], m);
57                 if (f[i+1][tov] >= f[i][j]+1){
58                     f[i+1][tov] = f[i][j]+1;
59                 }
60                 Min(g[i+1][tov], f[i][j]+1);
61                 tov = std::max(j-dw[i], 0);
62                 if (f[i+1][tov] > f[i][j]&&tov>=l[i+1]&&tov<=r[i+1]){
63                     f[i+1][tov] = f[i][j];
64                 }
65             }
66         if (!kick) break;
67     }
68     if (!kick){
69         puts("0"), ans = 0;
70         for (int i=1; i<=mx; i++)
71             if (l[i]!=1||r[i]!=m) ans++;
72         printf("%d\n",ans);
73     }else{
74         puts("1");
75         for (int i=1; i<=m; i++) Min(ans, f[n][i]);
76         printf("%d\n",ans);
77     }
78     return 0;
79 }
View Code

看了一下博客,应该说做法大同小异,没什么要补充的。这块还是以加强代码能力为主吧。

【模拟】bzoj1686: [Usaco2005 Open]Waves 波纹

嗯……联赛之前做模拟题这件事是该提上日程了。

概述「贪心“反悔”策略」模型

这里讲了两个相对基础的“反悔”问题。

其实“反悔”的大致思想不难理解,难就难在对于问题的转化和如何合理“反悔”。因为有些转化实际上是不充要的,所以这一步要严谨。

有些时候可能还是要靠感性理解吧……不过这个“感性”也要建立在起码的合理上,比如多对拍一下;打表看看规律之类的。

这里有道最近做的反悔贪心,感觉有些抽象,存在这里先吧……

【计数】7.11跳棋

这题思路很妙啊。

突破点在于考虑到相邻两个棋子最多只需要1个空位,之后的考虑棋子同质这个角度也非常好。

【二分 贪心】bzoj3477: [Usaco2014 Mar]Sabotage

平均值最值的问题算是一种套路吧。然后二分check的过程大同小异,图上是找正/负环;序列就是找个最值正/负序列。

【倍增】7.11fusion

现在看这题大致有点思路。但估计还没有考场上码出的代码能力……

整个思路流程大致是这样的:因为消去的顺序从左至右,那么一个区间若能消去,可以看做是一个一个互不影响的子区间被逐个消去,并且如果左端点固定,接下去消去的过程也是唯一的。由此想到预处理出$[i,nxt[i])$表示以$i$为左端点所能消去的最近右端点。那么查询时候就是一直跳$nxt[i]$,当且仅当存在$nxt[i]==r+1$时区间能够被消除。

但是这个只能算是一个优化,对于算法复杂度并没有实质性的改变,zzzzz.....的数据就能轻松卡掉。

由此便想到对$nxt[i]$倍增。

既然已经有了大致思路,剩下的就是细节处理了。所以还要加强各种代码能力。

【图论 搜索】bzoj1064: [Noi2008]假面舞会

存在环的情况是比较好判断的,最大值就是所有环长的gcd;最小值应该是最大值的大于三的最小质因数。

不存在环的情况好像没什么想法。

 

什么鬼好像zz了,不存在环的情况就是每个连通块的DAG的最长链总和。

然后这里对于找环有个小技巧,就是原边保留权值为1;建反向边权值为-1.第二次经过说明环长度为两次标记权值之差。

干脆再码了一遍

 1 #include<bits/stdc++.h>
 2 const int maxn = 100035;
 3 const int maxm = 1000035;
 4 
 5 int n,m,dis[maxn],mn,mx,chain,ans;
 6 bool vis[maxn];
 7 std::vector<int> f[maxn],g[maxn];
 8 std::pair<int, int> ev[maxm];
 9 
10 int read()
11 {
12     char ch = getchar();
13     int num = 0;
14     bool fl = 0;
15     for (; !isdigit(ch); ch=getchar())
16         if (ch=='-') fl = 1;
17     for (; isdigit(ch); ch=getchar())
18         num = (num<<1)+(num<<3)+ch-48;
19     if (fl) num = -num;
20     return num;
21 }
22 inline int abs(int x){return x>0?x:-x;}
23 int gcd(int a, int b){return b==0?a:gcd(b, a%b);}
24 void dfs(int x, int c)
25 {
26     if (vis[x]){
27         ans = gcd(abs(c-dis[x]), ans);
28         return;
29     }
30     vis[x] = 1, dis[x] = c;
31     mn = std::min(mn, c), mx = std::max(mx, c);
32     for (int i=0; i<f[x].size(); i++)
33         dfs(f[x][i], c+1);
34     for (int i=0; i<g[x].size(); i++)
35         dfs(g[x][i], c-1);
36 }
37 int main()
38 {
39     n = read(), m = read(), ev[0].first = -1, ev[1].second = -1;
40     for (int i=1; i<=m; i++) ev[i].first = read(), ev[i].second = read();
41     std::sort(ev+1, ev+m+1);
42     for (int i=1; i<=m; i++)
43         if (ev[i]!=ev[i-1]){
44             int u = ev[i].first, v = ev[i].second;
45             f[u].push_back(v), g[v].push_back(u);
46         }
47     for (int i=1; i<=n; i++)
48         if (!vis[i]){
49             mn = mx = 0;
50             dfs(i, 0);
51             chain += mx-mn+1;
52         }
53     if (ans >= 3){
54         for (int i=3; ; i++)
55             if (ans%i==0){
56                 printf("%d %d\n",ans,i);
57                 return 0;
58             }
59     }
60     if (chain>=3&&!ans) printf("%d %d\n",chain,3);
61     else puts("-1 -1");
62     return 0;
63 }
View Code

【最短路径树】51nod1443 路径和树

最短路径树这种模型的一个模板。正确性由每一步的贪心得到。

【树链剖分 差分】bzoj3626: [LNOI2014]LCA

将LCA的深度转为LCA到根的距离,这个转化挺不错的;至于差分应该说是套路了。

其他就是大力数据结构了。

  1 #include<bits/stdc++.h>
  2 const int maxn = 50035;
  3 const int maxm = 50035;
  4 const int MO = 201314;
  5 
  6 struct QRs
  7 {
  8     int x,z,id,opt;
  9     QRs(int a=0, int b=0, int c=0, int d=0):x(a),z(b),id(c),opt(d) {}
 10     bool operator < (QRs a) const
 11     {
 12         return x < a.x;
 13     }
 14 }q[maxn<<1];
 15 struct node
 16 {
 17     int tot,son,top,fa;
 18 }a[maxn];
 19 int n,m,tot,ans[maxn];
 20 int f[maxn<<2],ads[maxn<<2];
 21 int chain[maxn],chTot;
 22 int edgeTot,head[maxn],edges[maxm],nxt[maxm];
 23 
 24 void addedge(int u, int v)
 25 {
 26     edges[++edgeTot] = v, nxt[edgeTot] = head[u], head[u] = edgeTot;
 27 }
 28 int read()
 29 {
 30     char ch = getchar();
 31     int num = 0;
 32     bool fl = 0;
 33     for (; !isdigit(ch); ch=getchar())
 34         if (ch=='-') fl = 1;
 35     for (; isdigit(ch); ch=getchar())
 36         num = (num<<1)+(num<<3)+ch-48;
 37     if (fl) num = -num;
 38     return num;
 39 }
 40 void dfs1(int x, int fa)
 41 {
 42     a[x].tot = 1, a[x].son = a[x].top = -1, a[x].fa = fa;
 43     for (int i=head[x]; i!=-1; i=nxt[i])
 44     {
 45         int v = edges[i];
 46         dfs1(v, x), a[x].tot += a[v].tot;
 47         if (a[x].son==-1||a[a[x].son].tot < a[v].tot) a[x].son = v;
 48     }
 49 }
 50 void dfs2(int x, int top)
 51 {
 52     a[x].top = top, chain[x] = ++chTot;
 53     if (a[x].son==-1) return;
 54     dfs2(a[x].son, top);
 55     for (int i=head[x]; i!=-1; i=nxt[i])
 56         if (edges[i]!=a[x].son) dfs2(edges[i], edges[i]);
 57 }
 58 void pushup(int rt)
 59 {
 60     f[rt] = (f[rt<<1]+f[rt<<1|1])%MO;
 61 }
 62 void pushdown(int rt, int lc, int rc)
 63 {
 64     if (ads[rt]){
 65         int t = ads[rt];
 66         ads[rt<<1] = (ads[rt<<1]+t)%MO, ads[rt<<1|1] = (ads[rt<<1|1]+t)%MO;
 67         f[rt<<1] = (f[rt<<1]+lc*t)%MO, f[rt<<1|1] = (f[rt<<1|1]+rc*t)%MO;
 68         ads[rt] = 0;
 69     }
 70 }
 71 int query(int rt, int L, int R, int l, int r)
 72 {
 73     if (L <= l&&r <= R) return f[rt];
 74     int mid = (l+r)>>1, ret = 0;
 75     pushdown(rt, mid-l+1, r-mid);
 76     if (L <= mid) ret += query(rt<<1, L, R, l, mid);
 77     if (R > mid) ret += query(rt<<1|1, L, R, mid+1, r);
 78     return ret;
 79 }
 80 void modify(int rt, int L, int R, int l, int r)
 81 {
 82     if (L <= l&&r <= R){
 83         f[rt] = (f[rt]+r-l+1)%MO, ads[rt]++;
 84         return;
 85     }
 86     int mid = (l+r)>>1;
 87     pushdown(rt, mid-l+1, r-mid);
 88     if (L <= mid) modify(rt<<1, L, R, l, mid);
 89     if (R > mid) modify(rt<<1|1, L, R, mid+1, r);
 90     pushup(rt);
 91 }
 92 void updateNode(int x, int y=1)
 93 {
 94     while (a[x].top!=a[y].top)
 95     {
 96         modify(1, chain[a[x].top], chain[x], 1, n);
 97         x = a[a[x].top].fa;
 98     }
 99     modify(1, chain[y], chain[x], 1, n);
100 }
101 int queryNode(int x, int y=1)
102 {
103     int ret = 0;
104     while (a[x].top!=a[y].top)
105     {
106         ret = (ret+query(1, chain[a[x].top], chain[x], 1, n))%MO;
107         x = a[a[x].top].fa;
108     }
109     ret = (ret+query(1, chain[y], chain[x], 1, n))%MO;
110     return ret;
111 }
112 int main()
113 {
114     memset(head, -1, sizeof head);
115     n = read(), m = read();
116     for (int i=2; i<=n; i++) addedge(read()+1, i);
117     dfs1(1, 0);
118     dfs2(1, 1);
119     for (int i=1; i<=m; i++)
120     {
121         int l = read()+1, r = read()+1, c = read()+1;
122         q[++tot] = QRs(l-1, c, i, -1);
123         q[++tot] = QRs(r, c, i, 1);
124     }
125     std::sort(q+1, q+tot+1);
126     for (int i=1, now=0; i<=tot; i++)
127     {
128         int pos = q[i].x, fnd = q[i].z, id = q[i].id, opt = q[i].opt;
129         while (now < pos) updateNode(++now);
130         ans[id] += opt*queryNode(fnd);
131     }
132     for (int i=1; i<=m; i++) printf("%d\n",(ans[i]+MO)%MO);
133     return 0;
134 }
10.27

【计数】51nod1677 treecnt

这个是将答案按边考虑贡献的思想。有些时候还要试着从问题的反面去看待。

【树形背包】bzoj4033: [HAOI2015]树上染色

按边拆贡献的思路同上,然后根据这个思路来做背包。

【树形dp】7.14城市

一样的拆贡献。为什么考试时候我没做出来啊

初涉「带权并查集」&&bzoj3376: [Usaco2004 Open]Cube Stacking 方块游戏

并查集大致可以分为普通、带权两种。带权并查集的特点在于记录了同个集合不同元素之间的关系。更新是等到查询时候延迟自根向下更新。

【单调栈 动态规划】bzoj1057: [ZJOI2007]棋盘制作

求解矩阵最大面积0/1矩形应该算是单调栈最基础的应用了。

与0/1矩阵相关的问题还有例如:

UVA12265 Selling Land:最大周长0/1矩形。  这个由于周长同样单调,和最大面积一样处理。

COCI 2018/2019 Strah:所有0/1矩形面积和。  好像是维护x,y,xy的系数,大力拆开维护  待填坑

初涉trie

比较基础的字符串问题

【图论 动态规划拆点】luoguP3953 逛公园

不知道想法对不对:先预处理一趟最短路,然后如果有必要的话可以重建图一下(把已经$dis[u]+w>dis[v]+k$的边$(u,v)$删掉)。$f[i][j]$表示到达点$i$时候距离为$dis[i]+j$的方案数。那么这样状态数量是$O(nk)$的,每个状态只会被经过一次,转移是$O(1)$的。

好吧果然比较NAIVE,没注意到一些转移上的拓扑顺序。这说明了先想清楚做法再开始码题的重要性……

这里一个反向设状态的思路很好啊

计数类动态规划的转移大概就是这个样子。状态$f[i]$可以表示为1 -> i的方案数;也可以表示为i -> n的方案数。

第一种表示通常是提前转移贡献,也就是说在处理$f[i]$的时候,它的贡献已经由$\sum{f[j]}$得到了,处理$f[i]$是为了用它去更新$f[k]$。

第二种表示通常是自己收集贡献。意思是处理$f[i]$的时候,考虑它连出去的所有$f[j]$状态,再使$f[i]=\sum{f[j]}$。

这个举个形象一些的例子就是bzoj1093: [ZJOI2007]最大半连通子图这个地方,$f[i]$两种不同的状态表示决定了刷答案时候是该dfs还是topo。

那么这样看来,在某些特殊情况下topo是比较麻烦的,而记忆化dfs几乎没有任何影响,所以i -> n的状态表示更优秀。

这个有些时候可能还是需要多从其他方面推敲才能想到的。

比较考察dp功底的好题。

【数位dp】bzoj3209: 花神的数论题

注意一些细节问题。

【单调栈 前缀和 异或】7.21序列求和

初步的想法:固定右端点来计数,也就是说在做单调栈的过程中来统计答案。那么如果是随机数据应该能起到不错的效果。但我好像不会计算$\sum_{i=l_1}^{l_2}\oplus_{j=i}^{r}j$……?

啧这个东西怎么拆位啊.xor不同于and/or的特性就在于它运算的过程中每一位都有可能再改变,而不是就此固定下来(所以如果and/or的话应该会好做很多)。难道靠这个特性做文章?

诶好吧,$a_i \le 10^9$的话分位做$log_2n$次每次$n$也可以接受……

 

看来的确应用到这个xor的特性,就转化成了左右端点异或前缀和在这一位不相同这个问题,于是在统计上配合前缀和,思路并不复杂。先前那个想法不是很完善,应该做两遍单调栈处理出$a[i]$为最大值的左右边界。这样在固定下$max\{a[i]\}$的情况下就只需要再枚举二进制位来算贡献。那么看来就是一个普通的拆位算贡献题了。

边界开闭区间的±1需要注意。

【数位dp】bzoj1833: [ZJOI2010]count 数字计数

一眼看去按位拆贡献算。和Fenwick很像,考虑左边的数字是否改变。

于是写了一下,代码长度比数位dp略短一些,时间效率差不多。

思路大同小异,不过我觉得按贡献算的代码会更直观一些。

 1 #include<bits/stdc++.h>
 2 typedef long long ll;
 3 
 4 ll a,b,pw[15],pre[15],nxt[15],ans[15];
 5 int f[15],n;
 6 
 7 void solve(ll x, int c)
 8 {
 9     ll num = x;
10     for (n=0; num; num/=10) f[++n] = num%10;
11     for (int i=n; i>=1; i--) pre[i] = pre[i+1]*10+f[i];
12     for (int i=1; i<=n; i++) nxt[i] = nxt[i-1]+pw[i-1]*f[i];
13     for (int i=1; i<=n; i++)
14     {
15         if (i!=n&&f[i]) ans[0] += c*pw[i-1];
16         for (int t=1; t<f[i]; t++)
17             ans[t] += c*pw[i-1];
18         ans[f[i]] += c*nxt[i-1]+c;
19     }
20     for (int i=1; i<=n; i++)
21     {
22         if (i!=n) ans[0] += c*(pre[i+1]-1)*pw[i-1];
23         for (int t=1; t<=9; t++)
24             ans[t] += c*pre[i+1]*pw[i-1];
25     }
26 }
27 int main()
28 {
29     scanf("%lld%lld",&a,&b);
30     pw[0] = 1;
31     for (int i=1; i<=15; i++) pw[i] = pw[i-1]*10ll;
32     solve(a-1, -1), solve(b, 1);
33     for (int i=0; i<=9; i++)
34         printf("%lld ",ans[i]);
35     return 0;
36 } 
拆贡献

【状态压缩dp】bzoj1087: [SCOI2005]互不侵犯King

此题的技巧在于预处理出下一行的合法状态。

这里也存在一个拓扑序的问题,那么不妨用$f[i][j][k]$表示第$i$行状态为$k$,还剩$j$个可放的方案数。像这样设状态就能记忆化搜索了。

时间效率也相差不大,代码少个700b左右。

 1 #include<bits/stdc++.h>
 2 typedef long long ll;
 3 
 4 ll f[13][103][1203];
 5 bool vis[13][103][1203];
 6 int n,k,mx,cnt[1203],st[1203];
 7 std::vector<int> a[1203];
 8 
 9 ll dp(int i, int j, int k)
10 {
11     if (vis[i][j][k]) return f[i][j][k];
12     vis[i][j][k] = 1;
13     if (i==n){
14         if (j) return 0;
15         else return f[i][j][k] = 1;
16     }
17     for (int p=0; p<a[k].size(); p++)
18         if (j >= cnt[a[k][p]])
19             f[i][j][k] += dp(i+1, j-cnt[a[k][p]], a[k][p]);
20     return f[i][j][k];
21 }
22 inline int legal(int x)
23 {
24     int ret = 0;
25     for (int i=0; i<n; i++)
26         if (((x>>i)&1)&&((x>>(i+1))&1)) return -1;
27         else if ((x>>i)&1) ret++;
28     return ret;
29 }
30 inline bool match(int x, int y)
31 {
32     if ((x&y)||(x&(y<<1))||(x&(y>>1))) return 0;
33     return 1;
34 }
35 void init()
36 {
37     for (int i=0; i<=mx; i++)
38         if ((cnt[i] = legal(i))!=-1) st[++st[0]] = i;
39     a[0].push_back(0);
40     for (int i=1; i<=st[0]; i++)
41         for (int j=1; j<i; j++)
42         {
43             if (match(st[i], st[j])){
44                 a[st[i]].push_back(st[j]), 
45                 a[st[j]].push_back(st[i]);
46             }
47         }
48 }
49 int main()
50 {
51     scanf("%d%d",&n,&k);
52     mx = (1<<n)-1, init();
53     printf("%lld\n",dp(0, k, 0));
54     return 0;
55 }
倒着设状态

可能需要注意的小细节:这种倒着来记忆化搜索的做法,一般来说就不需要给每一个值一个“初始值”了。答案由后继状态倒推贡献而来。

【动态规划】bzoj1575: [Usaco2009 Jan]气象牛Baric

等等为什么当时我写了三维dp,好像有点没看懂。

反正遇到一些难处理的转移考虑预处理。

【数位dp】bzoj1799: [Ahoi2009]self 同类分布

数位dp不要虚。发现事情不对就多增状态;发现事情还是不对就再多枚举点东西。重点是心态不要乱。

【线段树 细节题】bzoj1067: [SCOI2007]降雨量

整个思路还是比较清晰的……不过细节需要好好注意吧。

初涉KMP算法

kmp基础.

板子里面注意是 while (t[i+1]!=t[j+1]&&j) j = fail[j]; 而不是 while (t[i+1]!=t[j+1]&&fail[j]) j = fail[j]; 因为无法匹配的时候$j=0$。

然后是一个完全/不完全最短循环节的性质。

再是一个 bzoj3670: [Noi2014]动物园 和 bzoj1511: [POI2006]OKR-Periods of Words 的巧妙思维题。

感觉字符串理论这一块并没有花太多时间……也不是很熟练。

联赛的字符串要求算是比较浅的吧,好好复习kmp、Aho、manacher应该问题不大。

【线段树 集合hash】bzoj4373: 算术天才⑨与等差数列

三次方是一种集合hash方法没错。此外如果集合没有特殊性质要求,还有一种方法是给每个元素rand一个long long出来,集合hash值是元素异或和。这个详情见bzoj3578: GTY的人类基因组计划2。

【数学 exgcd】bzoj1407: [Noi2002]Savage

exgcd的模板

【数学 裴蜀定理】bzoj2257: [Jsoi2009]瓶子和燃料

同样是一种拆分贡献的思想。每个数会对它的不同因子产生贡献,然而这个贡献又是不可共存的。

但这里因为因子之间相互独立,所以并不妨碍我们同时记上贡献。因此只需要在最后再来检查一遍就行了。

【树论 倍增】51nod1709 复杂度分析

依然是拆分贡献的想法。

因为这里二进制位和倍增有很大相性,所以考虑倍增求解。注意到高低位是同质的,那么总体流程就是先枚举每一个二进制位,再从根往下统计贡献。

用$f[i][j]$表示距点$i$距离为$2^j-1$的祖先,那么对于$i$来说,只有root...f[i][j]这些祖先会对第$j$个二进制位产生贡献。

首先考虑此时必定有贡献的祖先$f[i][j]...fa[f[i][j-1]]$,当这些祖先作为$i$和另一个点$k$的LCA时会产生贡献。那么显而易见的是这部分的贡献是$tot[f[i][j]]-tot[f[i][j-1]]$。

再考虑$root...fa[f[i][j]]$这部分可能产生的贡献。注意到$dis[i][f[i][j]]$在二进制下恰好是$j$位1,也就是说对于$f[i][j]$再向上的其他节点,$1...j$二进制位又重新从0计数。那么在只考虑第$j$位的情况下,对于$i$的这部分贡献和对于$f[i][j]$的总贡献是相等的。

一些细节:为了防止上跳时候“溢出”,root的父亲应设为root自身。

时间复杂度:$O(n\log n)$

 1 #include<bits/stdc++.h>
 2 typedef long long ll;
 3 const int maxn = 100035;
 4 const int maxm = 200035;
 5 
 6 ll w[maxn],ans;
 7 int n,fa[maxn],tot[maxn];
 8 int f[maxn][23],dep[maxn],chain[maxn],chTot;
 9 int edgeTot,head[maxn],nxt[maxm],edges[maxm];
10 
11 void addedge(int u, int v)
12 {
13     edges[++edgeTot] = v, nxt[edgeTot] = head[u], head[u] = edgeTot; 
14     edges[++edgeTot] = u, nxt[edgeTot] = head[v], head[v] = edgeTot; 
15 }
16 int read()
17 {
18     char ch = getchar();
19     int num = 0;
20     bool fl = 0;
21     for (; !isdigit(ch); ch = getchar())
22         if (ch=='-') fl = 1;
23     for (; isdigit(ch); ch = getchar())
24         num = (num<<1)+(num<<3)+ch-48;
25     if (fl) num = -num;
26     return num;
27 }
28 void dfs(int x, int fat)
29 {
30     tot[x] = 1, chain[++chTot] = x;
31     dep[x] = dep[fat]+1, f[x][0] = x, f[x][1] = fa[x] = fat;
32     for (int i=head[x]; i!=-1; i=nxt[i])
33     {
34         int v = edges[i];
35         if (v!=fat) dfs(v, x), tot[x] += tot[v];
36     }
37 }
38 int main()
39 {
40     memset(head, -1, sizeof head);
41     n = read();
42     for (int i=1; i<n; i++) addedge(read(), read());
43     dfs(1, 1);
44     for (int j=2; j<=20; j++)
45         for (int i=1; i<=n; i++)
46             f[i][j] = fa[f[f[i][j-1]][j-1]];
47     for (int j=1; j<=20; j++)
48     {
49         memset(w, 0, sizeof w);
50         for (int ix=1, i=chain[ix]; ix<=n; i=chain[++ix])
51         {
52             w[i] = tot[f[i][j]]-tot[f[i][j-1]];
53             if (dep[i] > (1<<j)) w[i] += w[fa[f[i][j]]];
54             ans += w[i];
55         }
56     }
57     printf("%lld\n",ans);
58     return 0;
59 }
View Code

【dp 状态压缩 单调栈】bzoj3591: 最长上升子序列

很有意思的状压dp

记得好像鏼爷在51nod上还出了一题加强版,n=31……不过那题做法就不是状压dp了

主要是用单调栈来构建状压dp的这第一步超级好,令人拍案叫绝。之后的检查合法、转移状态等过程就是比较常规的操作了。

夏末八月

 

posted @ 2018-10-22 18:28  AntiQuality  阅读(303)  评论(0编辑  收藏  举报