【过题记录】7.31(树形dp,根号分治)

先补一下昨天没来得及写的题目

延时操控


分析:

由于是延时操控
所以敌人的前面几步跟我们走的是一样的
所不一样的是我们比敌人多走了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(nn/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
*/

通知

根号分治


  • 29
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值