ybt 题目总结&吐槽 合集(下)
第五章 动态规划
树形DP
T24 权值统计
树形dp阴间起来基本分两种,一种是结合别的知识(比如中篇的强联通分量),一种是混乱且又臭又长的递推状态。此题两者兼有一点。
惯例设
f
i
f_i
fi表示以
i
i
i为一端的
i
i
i子树内的结果,那么对于一个点
i
i
i,有关的答案只有两种,一种是以它为一端(即
f
i
f_i
fi),一种是经过这点,后者具有一个性质:在这部分产生的价值为任意两个儿子的
f
f
f的积再乘上这点的权值
a
i
a_i
ai.考虑怎么求这个价值,根据
(
a
1
+
a
2
+
a
3
+
.
.
.
+
a
k
)
2
=
∑
i
=
1
k
a
i
2
+
2
×
∑
i
=
1
n
∑
j
=
i
+
1
n
(
a
i
∗
a
j
)
(a_1+a_2+a_3+...+a_k)^2=\sum\limits_{i=1}^{k}a_i\,^2+2\times\sum\limits_{i=1}^n\sum\limits_{j=i+1}^n (a_i*a_j)
(a1+a2+a3+...+ak)2=i=1∑kai2+2×i=1∑nj=i+1∑n(ai∗aj),前面的平方的和与和的平方都不难求,所以可以用这个表示出这个价值,这道题就能够
O
(
n
)
O(n)
O(n)求解了。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5 + 1;
const int mod = 10086;
struct yjx{
int nxt,to;
}e[N << 1];
long long ecnt = -1,head[N],up[N],sum[N],c[N],res;
void save(int x,int y){
e[++ecnt].nxt = head[x];
e[ecnt].to = y;
head[x] = ecnt;
}
void dfs(int now,int fa){//稍微整理了一下格式
int i,temp;
up[now] = c[now];
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(temp == fa) continue;
dfs(temp,now);
up[now] = (up[now] + (up[temp] * c[now] % mod)) % mod;
sum[now] = (sum[now] + up[temp] * (up[now] - up[temp] * c[now] % mod + mod) % mod) % mod;
}
sum[now] = (sum[now] + c[now]) % mod;
res = (res + sum[now]) % mod;
}
int main(){
int i,n,x,y;
scanf("%d",&n);
memset(head,-1,sizeof(head));
for(i = 1;i <= n;i++){
scanf("%lld",&c[i]);
}
for(i = 1;i < n;i++){
scanf("%d %d",&x,&y);
save(x,y),save(y,x);
}
dfs(1,0);
printf("%lld\n",res);
return 0;
}
T25 树的合并
求直径是一个老生常谈的话题了。
在中篇就涉及过一道有关直径的题,但是那道题是形成直径,并非在完整的树上寻找直径。总结一下寻找直径的方法:一种是树形DP直接求,另一种是两次dfs找到两个端点,前者适合求直径长度以及一些衍生的链长问题,后者适合求端点。其实都是
O
(
n
)
O(n)
O(n)的,不过写起来复杂程度不同。此题显然用的是前一种。
直径就是树上最长链,为了能够达到
O
(
n
)
O(n)
O(n)的效果,记
f
i
f_i
fi、
g
i
g_i
gi分别表示
i
i
i子树内和子树外的最长链长,那么
d
=
m
a
x
(
f
i
,
g
i
)
d= max(f_i,g_i)
d=max(fi,gi).
f
i
f_i
fi并不难求,关键在于如何求
g
i
g_i
gi.
g
i
g_i
gi显然需要用它的父节点更新而来。能够更新
g
i
g_i
gi的有两种,一种是
g
f
a
g_{fa}
gfa,一种是除
i
i
i这个子树的最长链。为了求后者,还需要维护一个子树内次长链
f
2
f2
f2。考虑到
f
i
f_i
fi和
f
2
i
f2_i
f2i是倒序更新,
g
i
g_i
gi是正序更新且需要前两个数组,所以需要两次dfs,此题又需要求两个树的直径,所以需要四次dfs(令人窒息)。
求直径(以一棵为例)代码如下:
void dfs11(int now,int fa){
int i,temp;
for(i = head1[now];~i;i = e1[i].nxt){
temp = e1[i].to;
if(temp == fa) continue;
dfs11(temp,now);
if(f[temp] + 1 > f[now]) f2[now] = f[now],f[now] = f[temp] + 1,pos[now] = temp;
else if(f[temp] + 1 > f2[now]) f2[now] = f[temp] + 1;
}
}
void dfs12(int now,int fa){
int i,temp;
if(pos[fa] == now) g[now] = max(g[fa],f2[fa]) + 1;
else g[now] = max(g[fa],f[fa]) + 1;
if(now == 1) g[now] = 0;
len1[now] = max(f[now],g[now]);//求以该点为一端的最长链长,后续有用
d1 = max(d1,len1[now]);//更新出这棵树的直径
for(i = head1[now];~i;i = e1[i].nxt){
temp = e1[i].to;
if(temp == fa) continue;
dfs12(temp,now);
}
}
两棵树合并后,新的直径显然只有两种可能,一种是原来两棵树较大的直径,另一种则是被连接的两端点的最长链长之和+1,即代码中的
l
e
n
1
[
i
]
+
l
e
n
2
[
j
]
+
1.
len1[i]+len2[j]+1.
len1[i]+len2[j]+1.问题在于,如果直接暴力找,时间复杂度是
O
(
n
m
)
O(nm)
O(nm)的,之前费这么多劲搞的
O
(
n
)
O(n)
O(n)求直径就白给了。
但是考虑到很多时候这个和并不会比直径更大,如果能够去掉这些情况,时间复杂度就得以优化。像单调队列一样思考,把两个
l
e
n
len
len数组进行排序,设两个指针,一个指1,一个指
n
n
n,枚举第一个指针
l
l
l,从大往小移动第二个指针
r
r
r,直到其和小于直径,那么
l
e
n
1
[
l
]
len1[l]
len1[l]与
l
e
n
2
[
r
]
len2[r]
len2[r]在此过程中产生的贡献就等于
(
(
n
−
r
)
×
(
l
e
n
1
[
l
]
+
1
)
+
r
×
d
+
∑
i
=
r
+
1
n
l
e
n
2
[
r
]
)
((n-r)\times (len1[l]+1)+r\times d+\sum\limits_{i=r+1}^{n} len2[r])
((n−r)×(len1[l]+1)+r×d+i=r+1∑nlen2[r])。接着增加
l
l
l,那么
r
r
r再次获得左移的机会,以此类推,最终的时间复杂度是
O
(
n
+
m
)
O(n+m)
O(n+m).如果追求线性(此题
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)级别可以通过),排序可以用桶排。
统计权值部分代码如下:
len2[0] = -2e9;
for(i = 1;i <= n;i++){
while(len1[i] + len2[r] + 1 > d && r >= 0){
pref += len2[r];
--r;
}
res += (len1[i] + 1) * (m - r) + pref + d * r;
}
状压DP
T26 涂抹果酱
此题首先要用一个性质
(否则就会写的和题解一样难看) :
对于一个三进制的数
S
S
S,它第
i
i
i位(位数从右至左自0开始)的数字即为
S
/
3
i
.
S / 3^i.
S/3i.
利用这一性质,就能快速地取出这位的数字而不必搞大量数组,也就不会写的非常混乱。
首先处理一下一行所有可能的方案(顺便以此判断一下第
k
k
k行的方案是否是合法的),设
d
p
(
i
,
S
)
dp(i,S)
dp(i,S)表示第
i
i
i行的状态为
S
S
S的方案数,那么这可以从所有与上一行不重合的方案当中转移而来。利用上面的这个性质,可以非常方便地判断出来。
总之是一个多进制的经典题(但是在没有这个性质的时候做起来会非常痛苦),做法十分标准,练习很合适。所以此题算是一个比较难的模板。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int mod = 1e6;
int n,m,k,cnt,st[251],mi[11];
long long res,dp[10001][251];
bool check(int x,int y){//判断两个状态是否重合
int i;
for(i = 0;i <= m - 1;i++){
if((x / mi[i]) % 3 == (y / mi[i]) % 3) return 0;
}
return 1;
}
int main(){
int i,j,l,stat = 0,x;
bool lipu;
scanf("%d %d",&n,&m);
scanf("%d",&k);
mi[0] = 1;
for(i = 1;i <= m;i++){
scanf("%d",&x);
stat = stat * 3 + x - 1;//求出第k行的状态
mi[i] = mi[i - 1] * 3;
}
for(i = 0;i < mi[m];i++){
lipu = 0;
for(j = 0;j <= m - 2;j++){
if((i / mi[j]) % 3 == (i / mi[j + 1]) % 3) lipu = 1;
//处理出所有合法的方案
}
if(!lipu){
st[++cnt] = i;
}
}
lipu = 1;
for(i = 1;i <= cnt;i++){
if(stat == st[i]){
lipu = 0;
break;
}
}
if(lipu){//第k行方案本身不合法
printf("0\n");
return 0;
}
if(k == 1) dp[1][stat] = 1;
else{
for(i = 1;i <= cnt;i++) dp[1][st[i]] = 1;
}
for(i = 2;i <= n;i++){
if(i == k){//特殊处理
for(j = 1;j <= cnt;j++){
if(check(st[j],stat)){
dp[i][stat] = (dp[i][stat] + dp[i - 1][st[j]]) % mod;
}
}
}
else if(i == k + 1){//另一个特殊处理
for(j = 1;j <= cnt;j++){
if(check(st[j],stat)){
dp[i][st[j]] = (dp[i][st[j]] + dp[i - 1][stat]) % mod;
}
}
}
else{
for(j = 1;j <= cnt;j++){
for(l = 1;l <= cnt;l++){
if(check(st[j],st[l])){
dp[i][st[j]] = (dp[i][st[j]] + dp[i - 1][st[l]]) % mod;
}
}
}
}
}
for(i = 1;i <= cnt;i++){
res = (res + dp[n][st[i]]) % mod;
}
printf("%lld\n",res);
return 0;
}
/*
2 2
1
2 3
*/
T27 炮兵阵地
(洛谷P2704)
一道没啥技巧纯粹硬核的经典问题。
我在做这题的时候还处在逐渐熟悉位运算的阶段,这就是为什么一道经典题能够进入这个合集的原因…
首先炮的打击范围一侧长度为2,所以需要预处理一下前两行的状态,同时顺理成章的需要同时记录两行的状态,设
d
p
(
i
,
S
,
T
)
dp(i,S,T)
dp(i,S,T)表示处理到第
i
i
i行,这一行的状态是
S
S
S,前一行的状态是
T
T
T最多放的炮个数。这样一来,只需枚举这行及其前两行这三行的状态,就可以完成转移。
接下来设计状态。首先是这个地形,为了便于判断非法,地形状态方面设山坡为1,平原为0,阵型状态方面设放了炮为1,不放为0,如果在山坡上放了炮,就反映为与运算的结果不为0。炮与炮之间的关系如何处理就是一个比较经典的问题了,直接判断几个状态的与运算结果即可,别忘了由于更新顺序的关系,只需考虑向上和向左,另两个方向不必关心。
另外,考虑到此题
n
≤
100
,
m
≤
10
n\leq 100,m\leq10
n≤100,m≤10,内存仅有125MB,开不下一个
n
×
2
m
+
1
n\times 2^{m+1}
n×2m+1的数组,所以需要用滚动数组从0~2滚动,要注意如何处理滚动数组下标和取模(至少我经常因此翻车)。
代码如下:
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
long long dp[4][1 << 10 + 1][1 << 10 + 1],a[101],num[1 << 10 + 1];
int count(int s){//计算当前状态1的个数
int ret = 0;
while(s){
if(s & 1) ++ret;
s >>= 1;
}
return ret;
}
int main(){
char s[11];
int i,j,k,l,n,m;
long long res = 0;
scanf("%d %d",&n,&m);
for(i = 0;i < n;i++){
scanf("%s",s);
for(j = 0;j < m;j++){
a[i] = a[i] | ((s[j] == 'H') << j);
}
}
for(i = 0;i < (1 << m);i++) num[i] = count(i);
for(i = 0;i < (1 << m);i++){
for(j = 0;j < (1 << m);j++){
if((i & a[0] || (j & a[1]) || (i & j) || (i & (i << 1) || (i & (i << 2)) || (j & (j << 1))) || (j & (j << 2)))) continue;
//非法状态,前三个是同一列2格内有炮,后四个是同一行2格内有炮
dp[1][j][i] = num[i] + num[j];
}
}
for(i = 2;i < n;i++){
for(j = 0;j < (1 << m);j++){//本行状态
if(j & a[i] || (j & (j << 1)) || (j & (j << 2))) continue;
for(k = 0;k < (1 << m);k++){//前一行状态
if(k & a[i - 1] || (j & k) || (k & (k << 1)) || (k & (k << 2))) continue;
for(l = 0;l < (1 << m);l++){//再前一行状态
if(l & a[i - 2] || (j & l) || (k & l) || (l & (l << 1)) || (l & (l << 2))) continue;
dp[i % 3][j][k] = max(dp[i % 3][j][k],dp[(i - 1) % 3][k][l] + num[j]);
}
}
}
}
for(i = 0;i < (1 << m);i++){
for(j = 0;j < (1 << m);j++){
res = max(res,dp[(n - 1) % 3][i][j]);
}
}
printf("%lld\n",res);
return 0;
}
T28 最短路径
一道比较奇妙的状压DP。
首先可以求两个特殊点之间的最短路,但是经过标记点就不是最短路能够胜任的了,此题数据范围并不大(特殊点不多于10个,加上起点终点也不超过12个),所以可以直接从每一个特殊点跑Dijkstra,对于特殊点是否被走过可以进行状压转移。
最短路部分没什么可讲的,对于状态,设走过为1,未走为0,每一次更新一个状态,都用没有走过这点的状态更新,也就是对这一位取异或。这样转移就完成了。
此题是一个状压的灵活运用的开始,做的时候感觉并不简单,状压有点难写,比较需要思考(好在是否要用状压仅仅需要看数据范围就足以得出)。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<functional>
#include<utility>
using namespace std;
typedef pair<int,int> pr;
const int N = 5e4 + 1;
const int M = 1e5 + 1;
int ecnt = -1,head[N],pos[13],vis[N];
long long dis[N],f[13][1 << 12],dist[13][13];
struct yjx{
int nxt,to,c;
}e[M];
void save(int x,int y,int w){
e[++ecnt].nxt = head[x];
e[ecnt].to = y;
e[ecnt].c = w;
head[x] = ecnt;
}
void Dijkstra(int st){//普通的最短路,记得每次清空
int i,now,temp;
priority_queue<pr,vector<pr>,greater<pr> > Q;
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[st] = 0;
Q.push(make_pair(0,st));
while(!Q.empty()){
now = Q.top().second;
Q.pop();
if(vis[now]) continue;
vis[now] = 1;
for(i = head[now];~i;i = e[i].nxt){
temp = e[i].to;
if(dis[temp] > dis[now] + e[i].c){
dis[temp] = dis[now] + e[i].c;
if(!vis[temp]) Q.push(make_pair(dis[temp],temp));
}
}
}
}
int main(){
int i,j,k,n,m,p,s,t,x,y,w;
scanf("%d %d %d %d %d",&n,&m,&p,&s,&t);
p += 2;
memset(head,-1,sizeof(head));
for(i = 1;i <= m;i++){
scanf("%d %d %d",&x,&y,&w);
save(x,y,w);
}
for(i = 2;i < p;i++){
scanf("%d",&pos[i]);
}
pos[1] = s,pos[p] = t;
for(i = 1;i <= p;i++){
Dijkstra(pos[i]);
for(j = 1;j <= p;j++){
dist[i][j] = dis[pos[j]];
}
}
if(dist[1][p] > 1e12){//不连通
printf("-1\n");
return 0;
}
memset(f,0x3f,sizeof(f));
f[1][1] = 0;
for(i = 1;i < (1 << p);i++){
for(j = 1;j <= p;j++){//转移点
if(!((i >> (j - 1)) & 1)) continue;//这一位为0,不能到达
for(k = 1;k <= p;k++){//目标点
if((i >> (k - 1)) & 1 || j == k) continue;//目标已经走到了
f[k][i ^ (1 << (k - 1))] = min(f[k][i ^ (1 << (k - 1))],f[j][i] + dist[j][k]);
}
}
}
printf("%lld\n",f[p][(1 << p) - 1]);
return 0;
}
T29 图的计数
一道写起来相当相当烦人的状压。
由于此题仅仅关心奇偶性,所以也就可以只考虑状态的奇偶性。设
f
(
i
,
j
)
f(i,j)
f(i,j)表示到第
i
i
i层奇偶状态为
j
j
j的方案数,
g
1
(
i
,
j
)
g1(i,j)
g1(i,j)表示第
i
i
i行考虑到
j
j
j时的可达性状态,
g
2
(
i
,
j
)
g2(i,j)
g2(i,j)则表示取反后的可达性状态,两者在更新的时候恰好逆过来。
具体操作直接写在注释里面了。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int mod = 998244353;
int g1[10001][11],g2[10001][11];
long long f[10001][1 << 10];
int main(){
int i,j,k,n,l,x,s1,s2,res = 0,tot;
scanf("%d %d",&n,&k);
for(i = 0;i < k;i++){
scanf("%d",&x);
g1[1][0] = g1[1][0] + (x << i);
}
for(i = 2;i < n - 1;i++){
for(j = 0;j < k;j++){
for(l = 0;l < k;l++){
scanf("%d",&x);
g1[i][j] = g1[i][j] + (x << l);
g2[i][l] = g2[i][l] + (x << j);//预处理
}
}
}
for(i = 0;i < k;i++){
scanf("%d",&x);
g1[n][0] = g1[n][0] + (x << i);
}
f[2][g1[1][0]] = 1;
for(i = 2;i < n;i++){
for(j = 0;j < (1 << k);j++){
if(f[i][j]){
s1 = 0,s2 = 0;
for(l = 0;l < k;l++){
if(j & (1 << l)){
s1 ^= g1[i][l],s2 ^= g2[i][l];//通过异或求得一部分点取反之后的状态
}
}
f[i + 1][s1] = (f[i + 1][s1] + f[i][j]) % mod;//累计状态数
f[i + 1][s2] = (f[i + 1][s2] + f[i][j]) % mod;
}
}
}
for(i = 0;i < (1 << k);i++){
tot = 0;
for(j = 0;j < k;j++){
if((g1[n][0] & (1 << j)) && (i & (1 << j))) tot ^= 1;
//求状态的奇偶性
}
if(!tot) res = (res + f[n - 1][i]) % mod;
}
printf("%d\n",res);
return 0;
}
单调队列
T30 粉刷木板
显然,为了方便,要首先把所有粉刷匠按照
S
i
S_i
Si排序。
设
d
p
(
i
,
j
)
dp(i,j)
dp(i,j)表示前
i
i
i个粉刷匠刷到第
j
j
j个木板的最大报酬,很明显,一个初始的转移为
d
p
(
i
,
j
)
=
m
a
x
(
d
p
(
i
−
1
,
j
)
,
d
p
(
i
,
j
−
1
)
)
dp(i,j)=max(dp(i-1,j),dp(i,j-1))
dp(i,j)=max(dp(i−1,j),dp(i,j−1)),除此之外,假设这个粉刷匠刷的区间为
[
k
+
1
,
j
]
[k+1,j]
[k+1,j],那么
d
p
(
i
,
j
)
=
m
a
x
(
f
(
i
−
1
,
k
)
+
p
i
×
(
j
−
k
)
)
(
j
−
L
i
≤
k
<
S
i
)
dp(i,j)=max(f(i-1,k)+p_i\times(j-k))\,\,\,(j-L_i\leq k <S_i)
dp(i,j)=max(f(i−1,k)+pi×(j−k))(j−Li≤k<Si),
化简得到
d
p
(
i
,
j
)
=
p
i
×
j
+
m
a
x
(
f
(
i
−
1
,
k
)
−
p
i
×
k
)
(
j
−
L
i
≤
k
<
S
i
)
dp(i,j)=p_i\times j+max(f(i-1,k)-p_i\times k)\,\,\,(j-L_i\leq k <S_i)
dp(i,j)=pi×j+max(f(i−1,k)−pi×k)(j−Li≤k<Si),由于
i
i
i是枚举得到的,发现取max的部分仅仅与
k
k
k有关,而
k
k
k是单调的,所以能用单调队列维护。
此题是例题2,虽然确实是一个中规中矩的单调队列,不过算是相当难办,比后面几个例题都要难处理,不知为啥要选这个当例题2…
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int M = 101;
const int N = 16001;
struct yjx{
int l,p,s;
}a[M];
bool cmp(yjx x,yjx y){
return x.s < y.s;
}
int f[M][N],Q[M];
int main(){
int i,j,k,n,m,front = 1,rear = 0;
scanf("%d %d",&n,&m);
for(i = 1;i <= m;i++){
scanf("%d %d %d",&a[i].l,&a[i].p,&a[i].s);
}
sort(a + 1,a + m + 1,cmp);
for(i = 1;i <= m;i++){
for(k = max(0,a[i].s - a[i].l);k < a[i].s;k++){
while(front <= rear && f[i - 1][Q[rear]] - Q[rear] * a[i].p <= f[i - 1][k] - k * a[i].p){
--rear;
}
Q[++rear] = k;//更新队尾
}
for(j = 1;j <= n;j++){
f[i][j] = max(f[i - 1][j],f[i][j - 1]);//先取一个初始值
if(j >= a[i].s){
while(front <= rear && j - Q[front] > a[i].l) ++front;
//维护合法区间
if(front <= rear) f[i][j] = max(f[i][j],(j - Q[front]) * a[i].p + f[i - 1][Q[front]]);
}
}
}
printf("%d\n",f[m][n]);
return 0;
}
T31 燃放烟火
(洛谷CF372C)
此题ybt的数据十分离谱,我样例都没过,交上去居然就AC了…
设
d
p
(
i
,
j
)
dp(i,j)
dp(i,j)表示放第
i
i
i个烟花,在
j
j
j位置获得的最大幸福度,那么显然有转移式:
d
p
(
i
,
j
)
=
m
a
x
(
d
p
(
i
−
1
,
k
)
+
b
i
−
∣
a
i
−
j
∣
)
(
j
−
d
×
Δ
t
≤
k
≤
j
+
d
×
Δ
t
)
dp(i,j)=max(dp(i-1,k)+b_i-|a_i-j|)\,(j - d\times\Delta t \leq k\leq j+d\times \Delta t)
dp(i,j)=max(dp(i−1,k)+bi−∣ai−j∣)(j−d×Δt≤k≤j+d×Δt)
k
k
k具有单调性,显然可以单调队列优化。为了便于维护两端的区间范围,可以正反各跑一次单调队列。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 150001;
const int M = 301;
int a[M],b[M],t[M],Q[N],f[2][N];
int main(){
int i,j,k,n,m,d,temp,front,rear,res = -1e9;
scanf("%d %d %d",&n,&m,&d);
for(i = 1;i <= m;i++){
scanf("%d %d %d",&a[i],&b[i],&t[i]);
temp = d * (t[i] - t[i - 1]);
front = 1,rear = 0;
for(j = 1;j <= n;j++){
while(front <= rear && Q[front] < j - temp) ++front;
while(front <= rear && f[0][Q[rear]] < f[0][j]) --rear;
Q[++rear] = j;
f[1][j] = f[0][Q[front]] + b[i] - abs(a[i] - j);
}
front = 1,rear = 0;
for(j = n;j >= 1;j--){
while(front <= rear && Q[front] > j + temp) ++front;
while(front <= rear && f[0][Q[rear]] < f[0][j]) --rear;
Q[++rear] = j;
f[1][j] = max(f[1][j],f[0][Q[front]] + b[i] - abs(a[i] - j));
}
for(j = 1;j <= n;j++) f[0][j] = f[1][j];
}
for(i = 1;i <= n;i++){
res = max(res,f[1][i]);
}
printf("%d\n",res);
return 0;
}
T32 出题方案
一道综合性比较强同时推导起来又非常恶心的单调队列
(难道纯粹的单调队列综合性还不够强?)
首先仍然考虑设计DP,从最简单的开始,设
d
p
i
dp_i
dpi表示到i为止的最小难度系数和,那么
d
p
i
=
d
p
j
+
a
k
(
j
+
1
≤
k
≤
i
)
.
dp_i=dp_j+a_k(j+1\leq k\leq i).
dpi=dpj+ak(j+1≤k≤i).显然
a
a
a数组是可以放进单调队列进行优化的(可以去除在范围以外的元素)。
不要忘了单调队列里面存的是下标,假设取出一个元素
b
k
b_k
bk,考虑到单调队列内的元素单调递减,那么在
b
k
b_k
bk到
b
k
+
1
b_{k+1}
bk+1以内的所有
d
p
dp
dp转移时取的都应该为
a
b
k
a_{b_k}
abk,考虑到
d
p
dp
dp应是单调不减的,为使
d
p
j
+
a
b
k
dp_j+a_{b_k}
dpj+abk最小,
d
p
dp
dp的下标应该为
b
k
−
1
b_{k-1}
bk−1.综上所述,我们维护出关于
a
a
a的(下标的)单调队列之后,需要再据此维护出一个
d
p
j
+
a
j
+
1
dp_j+a_{j+1}
dpj+aj+1的集合,从中取最小值进行更新,这个动态取最小值的过程可以用multiset完成。
所以此题不仅仅是单调队列,还要利用其中的下标再维护一个multiset,相当于比一般的单调队列复杂了一倍,如果忘记单调队列实际上存的是下标而非数组,很容易卡在里面出不来(至少我是这样)。此题堪称一道难题。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<set>
#include<cassert>
using namespace std;
const int N = 3e5 + 1;
multiset<int> S;
int n,m,Q[N],a[N],sum[N],f[N];
int main(){
int i,j = 0,k,front = 1,rear = 0;
scanf("%d %d",&n,&m);
for(i = 1;i <= n;i++){
scanf("%d",&a[i]);
sum[i] = sum[i - 1] + a[i];
}
for(i = 0;i <= n;i++){
while(sum[i] - sum[j] > m) ++j;//区间和过大,缩小
while(front <= rear && Q[front] < j){
if(rear - front > 0){
S.erase(S.find(f[Q[front]] + a[Q[front + 1]]));
//先维护multiset,再维护维护multiset的单调队列
}
++front;
}
while(front <= rear && a[Q[rear]] < a[i]){
if(rear - front > 0){
S.erase(S.find(f[Q[rear - 1]] + a[Q[rear]]));
}
--rear;
}
if(front <= rear) S.insert(f[Q[rear]] + a[i]);
Q[++rear] = i;
f[i] = f[j] + a[Q[front]];
if(rear - front > 0) f[i] = min(*S.begin(),f[i]);
}
printf("%d\n",f[n]);
return 0;
}
第六章 数学
质数与约数
T33 灯光控制
在做这道题之前,我们需要清楚一个性质:一个数
N
N
N至多有一个大于
N
\sqrt{N}
N的质因子。
对于每一盏灯,控制它的开关即为它的质因子。根据上面提到的这个性质,对于每一盏灯,最多只会被大于
N
\sqrt{N}
N的一个开关操作一次,因此假如我们处理出一种所有灯被小于
N
\sqrt{N}
N的开关控制的情况,对于剩下的开关,只需暴力枚举打开之后灯的数目是否会变多,变多则打开。
考虑到
N
≤
25
\sqrt{N}\leq25
N≤25,所以实际上涉及到的质数只有9个,完全可以搜索出所有的开关情况然后处理剩下的开关,时间上足够了。
考虑到质数与因数这一章的整体代码量都不大,这个dfs+枚举已经算是实现难度高的了。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 1001;
int n,m,res,tot,cnt,prime[1001],ctrl[1001],light[1001];
bool isprime[1001],vis[1001];
void Euler(int n){
int i,j;
memset(isprime,1,sizeof(isprime));
isprime[1] = 0;
for(i = 2;i <= n;i++){
if(isprime[i]) prime[++tot] = i;
for(j = 1;j <= tot && i * prime[j] <= n;j++){
isprime[i * prime[j]] = 1;
if(i % prime[j] == 0) break;
}
}
}
void solve(){
int i,j,sum = 0,temp;
for(i = 1;i <= n;i++) sum += light[i];
for(i = 1;i <= m;i++){
if(ctrl[i] > (int)sqrt(n)){
temp = 0;
for(j = 1;ctrl[i] * j <= n;j++) temp += light[ctrl[i] * j];
//printf("%d %d\n",temp,ctrl[i]);
if(n / ctrl[i] - temp > temp) sum += n / ctrl[i] - (temp << 1);
}
}
res = max(res,sum);
}
void dfs(int id){
//printf("%d %d?\n",id,cnt);
if(id > cnt){
solve();
return;
}
dfs(id + 1);
int i;
//printf("%d\n",prime[id]);
if(vis[prime[id]]){
for(i = 1;i * prime[id] <= n;i++) light[i * prime[id]] ^= 1;
dfs(id + 1);
for(i = 1;i * prime[id] <= n;i++) light[i * prime[id]] ^= 1;
}
}
int main(){
int i,t,x;
scanf("%d",&t);
while(t--){
tot = 0,cnt = 0;
memset(light,0,sizeof(light));
memset(vis,0,sizeof(vis));
scanf("%d %d",&n,&m);
Euler(n);
for(i = 2;i <= (int)sqrt(n);i++){
if(isprime[i]) ++cnt;
}
for(i = 1;i <= m;i++){
scanf("%d",&ctrl[i]);
vis[ctrl[i]] = 1;
}
res = 0;
dfs(1);
printf("%d\n",res);
}
}
博弈论
T34 剪纸游戏
一个标准的SG函数问题。
首先要思考必败态,由于SG函数要求一个必败态,而只知道必胜态,所以需要琢磨一下把哪些设为必败态,根据举例,发现可以把
(
2
,
2
)
(
2
,
3
)
(
3
,
2
)
(2,2)\,\,(2,3)\,\,(3,2)
(2,2)(2,3)(3,2)作为必败态。
剩下的就非常套路了,对于一张
n
×
m
n\times m
n×m的纸,其后续状态只有
n
×
(
m
−
i
)
n\times(m-i)
n×(m−i)以及
(
n
−
j
)
×
m
(n-j)\times m
(n−j)×m两种,对这两种状态SG值异或,就能求出当前状态的SG值。对所有的SG值作mex运算,就能求出这张
n
×
m
n\times m
n×m的纸的SG函数值。
总之SG函数对于我是一个自带高难度的东西,因此这题作为一个模板而上榜。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
int a[101],sg[1001][1001];
int SG(int n,int m){
if(sg[n][m] != -1) return sg[n][m];
int i,k = 0,ret = 0;
bool flag[2001];
memset(flag,0,sizeof(flag));
for(i = 2;2 * i <= n;i++){
flag[SG(i,m) ^ SG(n - i,m)] = 1;
//求两个后续状态对应的当前状态的SG值
}
for(i = 2;2 * i <= m;i++){
flag[SG(n,i) ^ SG(n,m - i)] = 1;
}
while(flag[ret]) ++ret;//mex运算
return sg[n][m] = sg[m][n] = ret;
}
int main(){
int i,n,m,res;
memset(sg,-1,sizeof(sg));
sg[2][2] = sg[2][3] = sg[3][2] = 0;
while(scanf("%d %d",&n,&m) != EOF){
res = SG(n,m);
if(res) printf("WIN\n");
else printf("LOSE\n");
}
return 0;
}
T35 砖块游戏
一道名副其实的、令人喷饭的阴间题。
首先这题的感觉非常像一个SG函数问题,考虑到这题的砖块只有1-4四种,不用SG函数这种阴间玩意儿,设
d
p
(
a
,
b
,
c
,
d
)
dp(a,b,c,d)
dp(a,b,c,d)表示长度为1-4的砖块分别有a-d种时先手是否必胜,显然,只要能够到达这一状态的存在任何一种必败态,该状态就一定是一个必胜态,那么大致的转移可以这样表示:
d
p
n
o
w
=
(
!
d
p
1
)
∣
(
!
d
p
2
)
∣
(
!
d
p
3
)
∣
.
.
.
∣
(
!
d
p
k
)
dp_{now}=(!dp_1)|(!dp_2)|(!dp_3)|...|(!dp_k)
dpnow=(!dp1)∣(!dp2)∣(!dp3)∣...∣(!dpk)
至于具体的转移,对于取走,有以下四种转移:
d
p
(
a
−
1
,
b
,
c
,
d
)
(
a
≥
1
)
dp(a-1,b,c,d)\,\,\,(a\geq1)
dp(a−1,b,c,d)(a≥1)
d
p
(
a
,
b
−
2
,
c
,
d
)
(
a
≥
2
)
dp(a,b-2,c,d)\,\,\,(a\geq2)
dp(a,b−2,c,d)(a≥2)
d
p
(
a
,
b
,
c
−
3
,
d
)
(
a
≥
3
)
dp(a,b,c-3,d)\,\,\,(a\geq3)
dp(a,b,c−3,d)(a≥3)
d
p
(
a
,
b
,
c
,
d
−
4
)
(
a
≥
4
)
dp(a,b,c,d-4)\,\,\,(a\geq4)
dp(a,b,c,d−4)(a≥4)
对于分解,有以下四种转移:
d
p
(
a
+
2
,
b
−
1
,
c
,
d
)
(
b
≥
1
)
dp(a+2,b-1,c,d)\,\,\,(b\geq1)
dp(a+2,b−1,c,d)(b≥1)
d
p
(
a
+
1
,
b
+
1
,
c
−
1
,
d
)
(
c
≥
1
)
dp(a+1,b+1,c-1,d)\,\,\,(c\geq1)
dp(a+1,b+1,c−1,d)(c≥1)
d
p
(
a
+
1
,
b
,
c
+
1
,
d
−
1
)
(
d
≥
1
)
dp(a+1,b,c+1,d-1)\,\,\,(d\geq1)
dp(a+1,b,c+1,d−1)(d≥1)
d
p
(
a
,
b
+
2
,
c
,
d
)
(
d
≥
1
)
dp(a,b+2,c,d)\,\,\,(d\geq1)
dp(a,b+2,c,d)(d≥1)
这样一来直接暴力枚举
a
b
c
d
abcd
abcd四维转移即可,显然时间复杂度是
O
(
a
b
c
d
)
O(abcd)
O(abcd),可以得70分。
离谱的是,这题的100分数据范围非常大(
1
0
10000
10^{10000}
1010000),显然我们需要一种基本是
O
(
1
)
O(1)
O(1)的算法。
考虑一下上面这几个转移式,经过一番离谱的观察 (想了很久都想不出来,无奈去看题解) ,设
r
=
(
a
+
c
)
m
o
d
2
,
s
=
(
b
+
d
)
m
o
d
3
r=(a+c)\,mod\,2,s=(b+d)\,mod\,3
r=(a+c)mod2,s=(b+d)mod3,会发现如果
r
≠
s
r\neq s
r=s,其差值只有-1,1,2三种,而对于每一种差值,总是存在两种方式使得
r
=
s
r=s
r=s,且互相需要的状态不重合。也就是每一次
r
≠
s
r\neq s
r=s,总有一种方式变为
r
=
s
r=s
r=s,因此
r
≠
s
r\neq s
r=s是必胜的,
r
=
s
r=s
r=s是必败的。
为了取模,需要一边读入一边取模,时间上足以通过。
总之这个优化非常离谱,不看题解真的想不到设这么两个玩意儿。只能说掌握70分怎么写,以及怎么推导必胜必败态就够了。不要忘了博弈论毕竟国赛级别的东西…
读入取模代码如下:
int read(int mod) {
char w;
while (w = getchar(), w < '0' || w > '9') {
}
int res = 0;
while (w >= '0' && w <= '9') {
res = (res * 10 + w - '0') % mod;
w = getchar();
}
return res;
}
T36 黑白棋
此题乍一看没什么思路,不妨结合样例思考一下:
我们可以发现几个规律:(以下全部假设白棋先动)如果这个棋子的分布形如"WWW.B"或者"WWWBB",白棋连续的棋子一定会被连续吃掉。为了避免这种情况,白棋一定要先移动剩下全部可以移动的棋子把先手推给黑棋,黑棋也同理,直到双方形成这种直接能决出胜负的局面。因此我们确定,在研究情况的时候,研究的是直接对峙的两个黑白棋(即最右侧的白棋和最左侧的黑棋)。
问题在于,以上两个例子具有特殊性,无法概括全部的情况。具体地说,这两个例子中前者需要仅有一个空格,后者需要黑棋一方有至少两个棋子,那么我们需要考虑没有这两个特殊情况的其他情况。
如果两个棋子的间距超过了1格,那么双方可以逐步向中间移动,如果移动了之后棋子的间距就是1格,那就变为第一种情况;否则,先手向前移动一步,仍然是1格间距,但是先后手变换。
如果出现了“WWWB.”,或者其实“WB.”也足够,那么白棋直接吃掉黑棋,变成第三种情况。
另外,如果出现一种".WBB",即不构成连吃威胁的局面,白棋必定是吃掉黑棋,如果黑棋吃掉白棋,仍然是第三种情况;否则转化成第四种,被白棋吃掉,再变回第三种。
最后总结一下,所有的情况最终都归为第三种,而第三种又都归为第一种或者第二种,我们只需要分类讨论后几个情况,然后进入第三种判断哪一方会被连续吃子输掉游戏即可。
总之此题的完整情况相当不好想,需要有非常清晰的思路,能够把个别归到整体,还得从样例中提取出关键信息,还得有比较清晰的实现思路。非常有意思的一道题。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e5 + 1;
int n;
char s[N];
bool Duel(bool ed){//W...B,ed=1表示白棋已经行动过
int i,l = 0,r = 0,len,temp;
long long step;
for(i = n;i >= 1;i--){
if(s[i] == 'W'){
l = i;
break;
}
}
if(!l) return 0;
for(i = 1;i <= n;i++){
if(s[i] == 'B'){
r = i;
break;
}
}//找到中间的两个棋子
if(!r) return 1;//黑棋已经被吃光了
step = ed;
//如果白棋已动,就产生了一个空位;也可以理解为黑棋要额外动一步,损失一步的机会
if(r - l > 2){
len = (r - l - 2) >> 1;
l += len,r -= len;
}
if(r - l > 2){
if(ed) r--;//先手前进一步
else l++;
}
for(i = l,temp = l;i >= 1;i--){
if(s[i] == 'W'){
step += temp - i;//计算白棋剩余可走步数
--temp;
}
}
for(i = r,temp = r;i <= n;i++){
if(s[i] == 'B'){
step -= i - temp;//计算白棋是否比黑棋的可走步数更多
++temp;
}
}
return step > 0;
}
bool Case1(int l,int r){//WB.
s[r] = 'W';
s[l] = '.';//吃棋
return Duel(1);
}
bool Case2(int l,int r){//WBB
int ret = 0;
s[l] = '.';
ret |= Duel(0);
s[l + 2] = '.';
ret |= Duel(0);
return ret;
}
bool solve(int l,int r){//分类讨论
if(r - l == 1){
if(r == n || s[r + 1] == '.') return Case1(l,r);
else if(l > 1 && s[l - 1] == 'W') return Duel(0);//WWBB
else return Case2(l,r);
}
else return Duel(0);
}
int main(){
int i,t,l,r;
scanf("%d",&t);
while(t--){
l = 0,r = 0;
scanf("%s",s + 1);
n = strlen(s + 1);
for(i = n;i >= 1;i--){
if(s[i] == 'W'){
l = i;
break;
}
}
for(i = 1;i <= n;i++){
if(s[i] == 'B'){
r = i;
break;
}
}
if(solve(l,r)) printf("W\n");
else printf("B\n");
}
return 0;
}
期望问题
T37 电影问题
期望是概率问题当中我认为目前接触到的最麻烦的一项,就在于它的计算方法往往比较超乎想象,初等表达式很容易推导出问题或者干脆推导不出来,结合dp之后难度反而会变低一点。期望也是我差的题目最多的一章(2道),实在做不出来了…
此题先简单概括一下:假如小明喜欢一部电影,就会看了这部电影;如果不喜欢这部电影,就会把以前喜欢看的看一遍。
首先这个看电影的顺序是我们自己定的,我们要找一个最好的方案。假设有两部电影
i
i
i和
j
j
j,先看
i
i
i后看
j
j
j,这时
i
i
i的期望价值就是
l
i
×
(
x
i
y
i
+
y
j
−
x
j
y
j
)
l_i\times (\frac{x_i}{y_i}+\frac{y_j-x_j}{y_j})
li×(yixi+yjyj−xj),
j
j
j同理。将两者进行比较,经过化简后发现,
l
i
×
x
i
y
i
×
y
j
−
x
j
y
j
>
l
j
×
x
j
y
j
×
y
i
−
x
i
y
i
l_i\times \frac{x_i}{y_i} \times \frac{y_j-x_j}{y_j}>l_j\times \frac{x_j}{y_j} \times \frac{y_i-x_i}{y_i}
li×yixi×yjyj−xj>lj×yjxj×yiyi−xi时,先看
i
i
i更好。由此,我们就能排出一个最佳的看电影顺序。
在解决排序的问题之后,我们需要计算一下每部电影的期望。喜欢一部电影会产生
l
i
×
x
i
y
i
l_i\times\frac{x_i}{y_i}
li×yixi的期望,而不喜欢一部电影先加上
−
l
i
×
y
i
−
x
i
y
i
-l_i\times\frac{y_i-x_i}{y_i}
−li×yiyi−xi的期望,而后会加上看之前喜欢的全部电影的期望,因此还需要一个
s
u
m
sum
sum记录一下之前喜欢每一部电影的期望。把这些加在一起就是一部电影产生的期望,全部电影的总期望也就可以计算了。
此题看题面逻辑非常简单明了,然而在期望的计算和决策上其实并不简单,翻车率很高。
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 2e6 + 2;
const int mod = 1004535809;
struct yjx{
long long l,x,y;
}a[N];
long long inv[N >> 1];
bool cmp(yjx p,yjx q){
return p.l * p.x * (q.y - q.x) > q.l * q.x * (p.y - p.x);
}
int main(){
int i,n;
long long res = 0,sum = 0;
scanf("%d",&n);
for(i = 1;i <= n;i++){
scanf("%lld %lld %lld",&a[i].l,&a[i].x,&a[i].y);
}
inv[1] = 1;
for (i = 2; i <= N >> 1; i++) {
inv[i] = inv[mod % i] * (mod - mod / i) % mod;//线性求逆元
}
sort(a + 1,a + n + 1,cmp);
for(i = 1;i <= n;i++){
res = (res + ((a[i].l * (a[i].x * 2 - a[i].y + mod) % mod + (a[i].y - a[i].x) * sum % mod)) * inv[a[i].y]) % mod;
//这是经过化简之后的式子
sum = (sum + a[i].l * a[i].x % mod * inv[a[i].y] % mod) % mod;//累计sum
}
printf("%lld\n",res);
return 0;
}
T38 彩球抽取
这题乍一看根本就无从下手,好像根本分析不出什么球的期望。
这道题其实是一道概率DP,我们从DP的角度分析就会找到一点思路。
设
d
p
(
i
,
j
,
k
)
dp(i,j,k)
dp(i,j,k)表示取到第
i
i
i轮,第
j
j
j个球有
k
k
k个的概率,那么答案统计的也就是
i
×
d
p
(
i
,
j
,
k
)
i\times dp(i,j,k)
i×dp(i,j,k)的总和。
考虑怎么转移这个dp,如果现在就考虑
j
j
j这个球,每次取出两个球,无非是四种情况:第一个是
j
j
j,第二个是
j
j
j,全是
j
j
j,无
j
.
j.
j.后两个其实可以算一种,因为都不改变
j
j
j的数量,第一种会使
j
j
j多一个,第二种会使
j
j
j少一个,此时概率DP的作用就发挥出来了,我们不必考虑具体这两个球怎么取,只需要直接从上一轮取球转移,如下:
for (j = 1; j <= 26; j++) {
for (k = 1; k < n; k++) {
temp = 1.0 * k * (n - k) / (1.0 * n * (n - 1));
/*temp表示n个球中有k个球j(当然实际总数并非k,只是从此转移),
取出一个球j的概率,有点组合数学的感觉*/
dp[1][j][k + 1] += dp[0][j][k] * temp;//j增加一个的概率
dp[1][j][k - 1] += dp[0][j][k] * temp;//j减少一个的概率
dp[1][j][k] += dp[0][j][k] * (1 - 2 * temp);//除去上两种情况的概率
}
}
通过这一种方式,就完成了一轮的转移。问题在于这个转移到底进行多少轮,一个奇妙的事情在于,dp一直在乘一个小数,随着轮数的增加这个dp的值会越来越小,直至在六位小数下能够被忽略。因此我们只需要看着时间限制尽可能多取几轮,轮数也不必记录,直接上滚动数组即可。
最终的代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int cnt[27];
double dp[2][27][27];
int main(){
int i,j,k,n;
char s[27];
double res = 0,temp;
scanf("%s",s + 1);
n = strlen(s + 1);
for(i = 1;i <= n;i++) ++cnt[s[i] - 'A' + 1];
for(i = 1;i <= 26;i++) dp[1][i][cnt[i]] = 1;
for(i = 0;i <= 100000;i++){
for(j = 1;j <= 26;j++){
res += i * dp[1][j][n];//累计答案
}
for(j = 1;j <= 26;j++){
for(k = 1;k <= n;k++){//转移滚动数组
dp[0][j][k] = dp[1][j][k];
dp[1][j][k] = 0;
}
}
for(j = 1;j <= 26;j++){
for(k = 1;k < n;k++){
temp = 1.0 * k * (n - k) / (1.0 * n * (n - 1));
dp[1][j][k + 1] += dp[0][j][k] * temp;
dp[1][j][k - 1] += dp[0][j][k] * temp;
dp[1][j][k] += dp[0][j][k] * (1 - 2 * temp);
}
}
}
printf("%.6lf\n",res);
return 0;
}
完结撒花!