SMSC2021 Day5&Day6
Day5
T1 矩阵 matrix (※差分)
差分技巧在区间操作上的应用。
对于一段序列
{
a
i
}
\{a_i\}
{ai} ,我们如果对下标处于
[
l
,
r
]
[l,r]
[l,r] 区间的所有元素增加
s
s
s,我们可以考虑
O
(
1
)
O(1)
O(1) 记录操作。新序列
{
b
i
}
\{b_i\}
{bi} 初始时全部元素为
0
0
0 ,在进行一次区间操作后,记录
b
l
←
b
l
+
s
,
b
r
+
1
←
b
r
+
1
−
s
b_l\leftarrow b_l + s,b_{r+1}\leftarrow b_{r+1}-s
bl←bl+s,br+1←br+1−s,如有若干次操作,亦同上处理。最后询问操作后的序列
{
a
i
′
}
\{a'_i\}
{ai′},那么对
{
b
i
}
\{b_i\}
{bi} 做一次前缀和得到
{
b
i
′
}
\{b'_i\}
{bi′} ,就有
a
i
′
=
a
i
+
b
i
′
a'_i=a_i+b'_i
ai′=ai+bi′。
差分可以高效地解决“多次区间操作,最终单次询问”的问题,步骤概括如下:
- 开一个差分数组
b[]
,每次接收操作 [ l , r ] + s [l,r]+s [l,r]+s,则b[l] += s , b[r+1] -= s
- 最终询问时,先对差分数组做前缀和操作
bs[i] = bs[i-1] + b[i]
- 最终得到答案
ans[i] = a[i] + bs[i]
回到本题,我们当然可以考虑每一次操作枚举行数
r
′
r'
r′ ,然后给每一行打上标记,但是这样复杂度在询问次数
q
q
q 比较大时太大,所以要考虑改进这个
O
(
n
q
)
O(nq)
O(nq) 的算法。
考虑上述算法的过程,如下图:
我们发现,在纵向也出现了区间操作,显然可以考虑在纵向对已有的差分标记再进行差分。对已有的差分数组b1[][]
再进行纵向差分的操作较为简单,但对其进行斜向差分的操作则不显而易见,但是我们仍然可以考虑设置一个新的斜向差分的数组 b2[][]
,计算最终表格时先将 b1[][]
纵向差分前缀和处理,再将 b1[][]
横向差分前缀和处理,最后对斜向差分数组 b2[][]
斜向前缀和处理,将所有差分数组相加即得到某个位置的元素。
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
int n,q,row,col,len,sum;
ll map[2005][2005],tmp,ans;
ll tag[2005][2005],ttag[2005][2005];
int main()
{
scanf("%d%d",&n,&q);
for(int t = 1;t <= q;t ++)
{
scanf("%d%d%d%d",&row,&col,&len,&sum);
tag[row][col] += sum;
ttag[row][col+1] -= sum;
tag[min(row+len,n+1)][col] -= sum;
ttag[min(n+1,row+len)][min(col+len+1,n+1)] += sum;
}
for(int i = 1;i <= n;i ++)
{
tmp = 0;
for(int j = 1;j <= n;j ++)
{
tmp += tag[i][j] + ttag[i][j];
map[i][j] = tmp;
tag[i + 1][j] += tag[i][j];
ttag[i + 1][j + 1] += ttag[i][j];
}
}
for(int i = 1;i <= n;i ++)
for(int j = 1;j <= n;j ++)
ans = ans^map[i][j];
printf("%lld",ans);
return 0;
}
Day6
T1 旅行 travel (※差分,树上二分与倍增)
可以将本题概括为“树上结点覆盖问题”,有多次操作单次最终询问的特点,可以考虑使用差分,具体操作如下:
- 对于每一个点 i i i 找出它能向上跳的最远的祖先 j j j
- 设
i
i
i 的父亲为
fa
(
i
)
\operatorname {fa}(i)
fa(i),差分标记
b[fa(i)] ++,b[fa(j)] ++
- 最后统计答案时是从叶子结点回溯到根结点,且过程中
bs
(
u
)
=
∑
i
∈
{
son
(
u
)
}
bs
(
i
)
\operatorname{bs}(u) = \sum_{i\in \{\operatorname{son}(u)\}}\operatorname{bs}(i)
bs(u)=∑i∈{son(u)}bs(i) 即
bs[u] += bs[son[u][i]]
至于向上跳的过程可以倍增预处理祖先和与祖先之间的距离。
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
int n;
ll d[200005],y;
int x,head[200005],cnt;
int f[200005][25];
ll di[200005][25];
int ans[200005];
struct edge{
int nxt;
int to;
ll dis;
}e[500005];
void prework(int u,int fa)
{
for(int i = 1;i <= 20;i ++)
{
f[u][i] = f[f[u][i-1]][i-1];
di[u][i] = di[u][i-1]+di[f[u][i-1]][i-1];
}
int v = 0;
for(int i = head[u];i;i = e[i].nxt)
{
v = e[i].to;
if(v == fa) continue;
f[v][0] = u;di[v][0] = e[i].dis;
prework(v,u);
}
return ;
}
void addedge(int from,int to,ll dis)
{
cnt ++;
e[cnt].nxt = head[from];
e[cnt].to = to;
e[cnt].dis = dis;
head[from] = cnt;
cnt ++;
e[cnt].nxt = head[to];
e[cnt].to = from;
e[cnt].dis = dis;
head[to] = cnt;
return ;
}
void dfs(int u)
{
if(u > 1 && d[u] >= di[u][0])
{
int x = u;int tmp = 0;
for(int i = 20;i >= 0;i --)
{
if(tmp + di[x][i] <= d[u])
{
tmp += di[x][i];
x = f[x][i];
}
}
ans[f[u][0]] ++;ans[f[x][0]] --;
}
for(int i = head[u];i;i = e[i].nxt)
{
if(f[u][0] != e[i].to)
{
dfs(e[i].to);
ans[u] += ans[e[i].to];
}
}
return ;
}
int main()
{
scanf("%d",&n);
for(int i = 1;i <= n;i ++)
scanf("%lld",&d[i]);
for(int i = 2;i <= n;i ++)
{
scanf("%d%lld",&x,&y);
addedge(i,x,y);
}
prework(1,0);
dfs(1);
for(int i = 1;i <= n;i ++)
printf("%d\n",ans[i]);
return 0;
}
T2 串串串 string (二分答案,状压 DP)
题目中有明显字眼提示求最大的最小值,果断考虑二分答案,显然要二分字符串的价值。字符串的价值可表示为
valstr
s
=
min
{
maxlen
(
c
i
)
}
\operatorname{valstr}_s=\min\{\operatorname{maxlen}(c_i)\}
valstrs=min{maxlen(ci)},也就意味着如果我们二分到一个字符串的价值为
m
i
d
mid
mid 那么其中前
k
k
k 个字母都必须至少出现
m
i
d
mid
mid 次,问题就在于如何判断给出的字符串能否满足以上条件。
观察到数据范围
k
⩽
17
k\leqslant 17
k⩽17 ,存在状压 DP 的可能性,但问题是如何设置动态规划的状态从而判断字符串满足条件。对于判定构造的存在性,我们不妨先转换一下思路,可以考虑将其转换为 DP 能做的计数问题或最优化问题,如计数符合要求的构造数目或者符合要求的构造的最小前缀。那么这里采用的是后者,我们可以考虑设
f
(
S
)
f(S)
f(S) 表示满足前
k
k
k 个字符是否出现
m
i
d
mid
mid 次的状态为
S
S
S 的最小位置,即
f
(
101
)
=
5
f(101)=5
f(101)=5 可以表示最早在字符串的前
5
5
5 位,可以满足第一个和第三个字母出现
m
i
d
mid
mid 次,但第二个字母出现不足
m
i
d
mid
mid 次。时间复杂度
O
(
2
k
⋅
k
)
O(2^k\cdot k)
O(2k⋅k)。
接下来的问题就变为如何转移,显然要知道前
k
k
k 个字母都在哪些位置可能连续出现
m
i
d
mid
mid 次,这时只需在 DP 前做一次预处理即可。为了降低程序运行的时间复杂度,我们可以预处理出对于某个字母在位置
i
i
i 时,离这个位置最近的转移位置。一种比较简便的办法是枚举每种字母,从后向前扫,如果?
或此字母的出现次数大于
m
i
d
mid
mid 就记录转移位置或者转移后所在的位置,预处理时间复杂度
O
(
n
k
)
O(nk)
O(nk)。
总时间复杂度
O
(
(
n
k
+
2
k
⋅
k
)
log
n
)
O((nk+2^k\cdot k)\log n)
O((nk+2k⋅k)logn)。
#include<iostream>
#include<cstdio>
#include<string>
#include<cstring>
using namespace std;
int n,k,lt,rt,mid,pos[20][200005],tmp,nxt,fir_pos,pre;
string s,stri[25],tmpstr;
char str[200005],tmpc;
bool flag1,able[20];
int f[1050000],tot,sta,ans;
void GetPos()
{
for(int i = 1;i <= k;i ++)
{
tmp = 0;pos[i][n + 3] = pos[i][n + 1] = pos[i][n + 2] = n + 2;
for(int j = n;j >= 1;j --)
{
if(str[j] == i+'a'-1 || str[j] == '?') tmp ++;
else tmp = 0;
if(tmp >= mid) pos[i][j] = j + mid - 1;
else pos[i][j] = pos[i][j + 1];
}
}
return ;
}
bool DP()
{
tot = ( 1 << k ) - 1 ;
for(int i = 1;i <= tot;i ++) f[i] = n + 2;
f[0] = 0;
for(int i = 1;i <= tot;i ++)
for(int j = 1;j <= k;j ++)
if((i&(1<<(j-1))) > 0)
{
sta = i - (1<<(j-1));
f[i] = min(f[i],pos[j][f[sta] + 1]);
}
if(f[tot] > n) return 0;
else return 1;
}
int main()
{
scanf("%d%d",&n,&k);
cin >> s;
for(int i = 0;i < s.size();i ++) str[i + 1] = s[i];
lt = 1;rt = n;
while(lt <= rt)
{
mid = (lt+rt)>>1;
GetPos();
if(DP())
{
ans = mid;
lt = mid + 1;
}
else rt = mid - 1;
}
printf("%d",ans);
return 0 ;
}