先补一下昨天没来得及写的题目
延时操控
分析:
由于是延时操控
所以敌人的前面几步跟我们走的是一样的
所不一样的是我们比敌人多走了k步
所以我们可以先让我们和敌人同步行走,最后让我们随机游走k步即可。
由于这里n和m的范围都很小,所以我们有一个初步的dp设想:
f
[
i
]
[
x
]
[
y
]
[
d
m
g
]
[
x
x
]
[
y
y
]
f[i][x][y][dmg][xx][yy]
f[i][x][y][dmg][xx][yy]
表示经过了i轮,我们走到了
(
x
,
y
)
(x,y)
(x,y),敌人走到了
(
x
x
,
y
y
)
(xx,yy)
(xx,yy),已经扣了敌人dmg滴血的方案数。
但是这样的复杂度显然会超。
我们尝试优化状态
其实,由于我们和敌人这时是同步走的,所以如果敌人不扣血,我们和敌人之间的位置差是一定的
就是初始的位置差。
这个时候我们可以直接确定敌人的位置。
但是如果敌人扣血了,那么位置偏差就会有改变。
最多会改变多少呢?
扣了几滴血,最多就会改变多少。
所以我们可以优化上述的dp状态为:
f
[
i
]
[
x
]
[
y
]
[
d
m
g
[
d
e
l
x
]
[
d
e
l
y
]
f[i][x][y][dmg[delx][dely]
f[i][x][y][dmg[delx][dely]
表示敌人的位置跟初始时候想比,x偏差了delx,y偏差了dely时的方案数。
这样就成功优化了状态。
同时当前的状态只能通过前一轮的状态转移过来,所以第一维我们又可以通过滚动数组去优化。
#include<iostream>
using namespace std;
#define int long long
const int N = 80,K = 5;
const int P = 1000000007;
int f[4][N][N][8][20][20],g[4][N][N];
int n,m,de,hp;
const int dx[4] = {0,0,1,-1};
const int dy[4] = {1,-1,0,0};
bool vi[N][N];
#define gc getchar()
void Clear(int ll){
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
for (int k = 0; k <= hp; k++)
for (int delx = -k; delx <= k; delx++){
int Max = k-abs(delx);
for (int dely = -Max; dely <= Max; dely++)
f[ll][i][j][k][delx+K][dely+K] = 0;
}
// for (int dely = 0; dely < 11; dely++)
// f[ll][i][j][k][delx][dely] = 0;
}
bool Check(int x,int y){
if (x < 1 || x > n || y < 1 || y > n) return 0;
if (vi[x][y] == 0) return 0;
return 1;
}
void Add(int &x,int y){
if (x+y < P) x = x+y; else x = x+y-P;
}
void Work(){
cin>>n>>m>>de>>hp;
Clear(0); Clear(1);
for (int l = 0; l < 2; l++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
g[l][i][j] = 0;
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) vi[i][j] = 1;
int stx,sty,edx,edy;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++){
char ch = gc;
while (ch!='#' && ch!='.' && ch!='P' && ch!='E') ch = gc;
if (ch == '.') continue;
if (ch == '#') vi[i][j] = 0;
if (ch == 'P') stx = i , sty = j;
if (ch == 'E') edx = i , edy = j;
}
edx-=stx; edy-=sty;
int O = 0;
f[0][stx][sty][0][K][K] = 1;
m-=de;
for (int l = 1; l <= m; l++){
Clear(O^1);
for (int i = 1; i <= n; i++){
for (int j = 1; j <= n; j++)
for (int k = 0; k < hp; k++)
for (int delx = -k; delx <= k; delx++){
int Max = k-abs(delx);
for (int dely = -Max; dely <= Max; dely++){
int now = f[O][i][j][k][delx+K][dely+K]; if (now == 0) continue;
for (int o = 0; o < 4; o++){
int xx = i+dx[o] , yy = j+dy[o];
if (Check(xx,yy) == 0) continue;
if (Check(xx+edx+delx,yy+edy+dely)) Add(f[O^1][xx][yy][k][delx+K][dely+K],now);
else if (k == hp-1) Add(g[0][xx][yy],now);
else Add(f[O^1][xx][yy][k+1][delx-dx[o]+K][dely-dy[o]+K],now);
}
}
}
}
O^=1;
}
O = 0;
for (int l = 0; l < de; l++){
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) g[O^1][i][j] = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++){
int now = g[O][i][j]; if (now == 0) continue;
for (int o = 0; o < 4; o++){
int x = i+dx[o] , y = j+dy[o];
if (Check(x,y) == 0) continue;
Add(g[O^1][x][y],now);
}
}
O^=1;
}
int ans = 0;
int l = O;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) Add(ans,g[l][i][j]);
cout<<ans<<endl;
return;
}
signed main(){
cin.tie(0);
ios::sync_with_stdio(false);
int t; cin>>t; while (t--) Work();
return 0;
}
小凸玩密室
分析:
这个题给了几个比较强的限制条件
完全二叉树,开了一个灯之后必须把当前子树所有灯都打开才能开子树外的灯。
这就决定了,我们如果想将子树x的灯全都打开,要么就是先开左子树的灯再开右子树的灯,那么就是反过来。
所以当前子树的答案中一定有左右两颗子树的贡献
然后还要再加上一项跨子树的贡献。这个子树是怎么跨的呢?
一定是从一棵树的叶子节点跨越到另一边的根节点中。
也就是说如果我们想计算当前的答案,我们需要知道他想从哪一个叶子节点跨越过来,这就形成了我们的dp状态:
f
[
u
]
[
x
]
f[u][x]
f[u][x]表示以u为根的子树,当前再叶子结点x结束的最小贡献。
如果我们想先访问左子树
那么我们就需要枚举左子树的结束节点x以及右子树的结束节点y,按照如下方式转移:
f
[
u
]
[
y
]
=
m
i
n
{
D
(
u
,
l
u
)
∗
A
[
l
u
]
+
f
[
l
u
]
[
x
]
+
D
(
x
,
r
u
)
∗
A
[
r
u
]
+
f
[
r
u
]
[
y
]
}
f[u][y]=min\{D(u,lu)*A[lu]+f[lu][x]+D(x,ru)*A[ru]+f[ru][y]\}
f[u][y]=min{D(u,lu)∗A[lu]+f[lu][x]+D(x,ru)∗A[ru]+f[ru][y]}
这样枚举x,y转移的复杂度就是
O
(
n
3
)
O(n^3)
O(n3)
还是会超时。
这个时候如果我们分解一下上式,我们发现,如果想要最小化
f
[
u
]
[
y
]
f[u][y]
f[u][y]
其实就是要最小化
f
[
l
u
]
[
x
]
+
D
(
x
,
u
)
∗
A
[
r
u
]
f[lu][x]+D(x,u)*A[ru]
f[lu][x]+D(x,u)∗A[ru]因为其他的几项可以当做常数处理
这个意思是,对于每一个y,他转移过来的对应的x都是一样的,就是那个最小值对应的minx
这个的最小值我们可以
O
(
叶子数
)
O(叶子数)
O(叶子数)预处理
而转移同样也是
O
(
叶子数
)
O(叶子数)
O(叶子数)
这颗二叉树一共有
O
(
l
o
g
n
)
O(logn)
O(logn)层,每一层的复杂度都是叶子个数
所以总的复杂度就是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
还有一个注意点,就是这个题不一定是从1开始的
所以还需要跑一个换根dp
确实有点麻烦
问题不大
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e6+100,inf = 1e19;
int n;
int a[N],b[N];
vector < int > dp[N],Dp[N],d[N];
#define pb push_back
int de[N];
void Dfs(int x){
de[x] = de[x/2]+b[x];
if (x*2 > n){
for (int i = x; i >= 1; i/=2)
dp[i].pb(0), Dp[i].pb(0), d[i].pb(de[x]-de[i]);
return;
}
Dfs(x*2);
int t = dp[x*2].size();
if (x*2+1 > n){
dp[x][0] = b[2*x]*a[2*x];
Dp[x][0] = dp[x][0];
return;
}
Dfs(x*2+1);
int ans1 = inf , ans2 = inf , Ans1 = inf , Ans2 = inf;
for (int i = 0; i < dp[x].size(); i++){
if (i < t){
ans1 = min(ans1,dp[x*2][i]+b[x*2]*a[x*2]+(d[x][i]+b[x*2+1])*a[x*2+1]);
Ans1 = min(Ans1,Dp[x*2][i]+d[x][i]*a[x]+b[2*x+1]*a[2*x+1]);
}
else{
ans2 = min(ans2,dp[x*2+1][i-t]+b[2*x+1]*a[2*x+1]+(d[x][i]+b[2*x])*a[2*x]);
Ans2 = min(Ans2,Dp[x*2+1][i-t]+d[x][i]*a[x]+b[2*x]*a[2*x]);
}
}
for (int i = 0; i < dp[x].size(); i++){
if (i < t){
dp[x][i] = ans2+dp[2*x][i];
Dp[x][i] = min(dp[x][i],Ans2+dp[2*x][i]);//换根
}
else{
dp[x][i] = ans1+dp[2*x+1][i-t];
Dp[x][i] = min(dp[x][i],Ans1+dp[2*x+1][i-t]);//换根
}
}
return ;
}
signed main(){
cin.tie(0);
ios::sync_with_stdio(false);
cin>>n;
for (int i = 1; i <= n; i++) cin>>a[i];
for (int i = 2; i <= n; i++) cin>>b[i];
Dfs(1);
int Min = inf;
for (int i = 0; i < Dp[1].size(); i++)
Min = min(Min,Dp[1][i]);
cout<<Min<<endl;
return 0;
}
哈希冲突
分析:
这是一道根号分治板题
我们先考虑暴力做法:
枚举起点x,不断加p跳跃
复杂度
O
(
n
∗
n
/
p
)
O(n*n/p)
O(n∗n/p)
而后在考虑一个dp做法:
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示起点为i,模数为j时的答案
想要求出这个dp的复杂度是
O
(
n
p
)
O(np)
O(np)
我们发现,一边的复杂度是
n
2
/
p
n^2/p
n2/p,一边是
n
p
np
np
怎么样能让两个的复杂度趋于稳定呢?
那就是让
n
2
/
p
=
n
p
n^2/p=np
n2/p=np
p=
(
n
)
\sqrt(n)
(n)
没错,这个时候p就是阈值,大于这个阈值,我们采用暴力
小于这个阈值,我们采用dp
O
(
1
)
O(1)
O(1)输出
总的复杂度
O
(
n
n
)
O(n\sqrt n)
O(nn)
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+100;
int n,m;
int a[N];
int s[5000][5000];
typedef pair < int , int > pii;
vector < pii > ch;
#define fi first
#define se second
int Calc(int st,int p){
int sum = 0;
for (int i = st; i <= n; i+=p)
sum+=a[i];
return sum;
}
int main(){
cin.tie(0);
ios::sync_with_stdio(false);
cin>>n>>m;
for (int i = 1; i <= n; i++) cin>>a[i];
int di = int(sqrt(n));
for (int p = 1; p <= di; p++)
for (int i = 1; i <= n; i++)
s[i%p][p]+=a[i];
for (int i = 1; i <= m; i++){
string s1; int x,y;
cin>>s1>>x>>y;
if (s1 == "A"){
if (x <= di) cout<<s[y%x][x]<<endl;
else cout<<Calc(y,x)<<endl;
}
else{
for (int p = 1; p <= di; p++) s[x%p][p]+=y-a[x];
a[x] = y;
}
}
return 0;
}
Remainder Problem
分析:
根上一题几乎一样
不同的是=变成+=
#include<bits/stdc++.h>
using namespace std;
//#define int long long
const int N = 5e5+10;
int q;
int s[5000][5000];
int a[N];
int Calc(int st,int p){
int sum = 0;
for (int i = st; i <= N-10; i+=p)
sum+=a[i];
return sum;
}
signed main(){
cin.tie(0);
ios::sync_with_stdio(false);
int di = (sqrt(N-100000));
cin>>q;
while (q--){
int op,x,y;
cin>>op>>x>>y;
if (op == 1){
a[x]+=y;
for (int i = 1; i <= di; i++) s[x%i][i]+=y;
}
else{
if (x <= di) cout<<s[y][x]<<endl;
else cout<<Calc(y,x)<<endl;
}
}
return 0;
}
Array Queries
其实思路大同小异
设
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示从位置i开始,步长为j时要跳的步数是多少。
f
[
i
]
[
j
]
=
f
[
i
+
a
[
i
]
+
j
]
[
j
]
+
1
f[i][j]=f[i+a[i]+j][j]+1
f[i][j]=f[i+a[i]+j][j]+1
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10,M = 318;
int f[N][M];
int n;
int a[N];
int Calc(int st,int p){
int cnt = 0;
while (st <= n) st = st+a[st]+p,cnt++;
return cnt;
}
int main(){
cin.tie(0);
ios::sync_with_stdio(false);
cin>>n;
for (int i = 1; i <= n; i++) cin>>a[i];
for (int p = 1; p < M; p++)
for (int i = n; i >= 1; i--)
if (i+a[i]+p > n) f[i][p] = 1;
else f[i][p] = f[i+a[i]+p][p]+1;
int q; cin>>q;
while (q--){
int p,k; cin>>p>>k;
if (p > n){
cout<<0<<endl; continue;
}
if (k < M) cout<<f[p][k]<<endl;
else cout<<Calc(p,k)<<endl;
}
return 0;
}
Sum of Progression
维护两个前缀和数组
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示从1以步长j跳到i时的答案,
s
[
i
]
[
j
]
s[i][j]
s[i][j]则表示单纯的前缀和数组
计算答案是多余的答案利用前缀和s数组减去即可
有一点需要注意的是因为这个题是多组数据
所以对于每一个n,我们都要单独计算阈值
不然会超时。
还有,最好小的那一维放前面,这样可以减少一些寻址的复杂度
#include<bits/stdc++.h>
using namespace std;
//#define int long long
#define ll long long
const int N = 1e5+10 , M = 318;
int n;
int a[N];
ll f[M][N];
ll s[M][N];
ll Calc(int now,int p,int k){
ll sum = 0;
for (int i = 0; i < k; i++){
sum+=1ll*a[now]*(i+1); now+=p;
}
return sum;
}
int q;
void Work(){
cin>>n>>q;
for (int i = 1; i <= n; i++) cin>>a[i];
int div = sqrt(n);
for (int p = 1; p <= div; p++)
for (int st = 1; st <= p; st++){
f[p][st] = a[st]; s[p][st] = a[st];
for (int i = st+p,j=2; i <= n; i+=p,j++)
f[p][i] = (f[p][i-p]+1ll*j*a[i]),s[p][i] = s[p][i-p]+1ll*a[i];
}
while (q--){
int now,p,k;
cin>>now>>p>>k;
int st = now%p;
if (st == 0) st = p;
int ed = now+(k-1)*p;
int C = (now-st)/p;
int la = max(0,now-p);
if (p <= div){
ll ans = f[p][ed]-f[p][la];
ans = ans-(s[p][ed]-s[p][la])*C;
cout<<ans<<' ';
}
else cout<<Calc(now,p,k)<<' ';
}
cout<<endl;
for (int p = 1; p < M; p++)
for (int i = 1; i <= n; i++) f[p][i] = 0 , s[p][i] = 0;
return;
}
signed main(){
cin.tie(0);
ios::sync_with_stdio(false);
int t; cin>>t; while (t--) Work();
return 0;
}
/*
5
3 1
-100000000 -100000000 -100000000
1 1 3
*/
通知
根号分治