五一普转提笔记Day-4

树形dp(自下而上)↓

T1

分析维度:f[i]表示以i为根的子树的所有点到i的距离之和。

初始化:f[1]=0

转移:

但是如果我们枚举每一个i,那就TLE了,此时我们就要使用换根dp,用原来一个点的结果在O(n)的时间内算出其他点的结果(自上而下),如下图:

完整代码:

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
const int maxn=1e5+1;
vector<int> z[maxn];
int f[maxn],ans,n;
int size[maxn];
void dfs(int now,int fa){
	for(auto x : z[now]){
		if(x!=fa){
			dfs(x,now);
		}
	}
	f[now]=0;
	size[now]=1;
	for(auto x : z[now]){
		if(x!=fa){
			size[now]+=size[x];
			f[now]+=f[x]+size[x];
		}
	}
}
void dfs(int now,int fa,int sum){
	ans=min(ans,f[now]+sum);
	for(auto x: z[now]){
		if(x!=fa){
			dfs(x,now,sum+f[now]-(f[x]+size[x])+(n-size[x]));
		}
	}
}
signed main(){
	cin>>n;
	for(int i=1;i<n;i++){  // 树有n-1条边
    	int u,v;
    	cin>>u>>v;
    	z[u].push_back(v);
    	z[v].push_back(u);
	}
	dfs(1,0);
	return 0;
}

T2求树上一点到其他点的距离和的和

分析维度:f[i]表示i的子树内所有经过i的路径的权值之和。

g[i]:i的子树内以i为起点的路径之和。

求值:

但是这里复杂度太高了,我们可以用一种更简单的方法去做(牛客输入不了公式,我只能截图了):

T3给定一棵树(连通无环图),其中某些节点被标记。定义:f[i]:节点 i 到所有被标记节点的最远距离。目标:找到所有 f[i]​ 的最小值,要求时间复杂度O(n)

分析维度:g[i]i的子树内被标记的节点到i的距离的最大值

方法:附上一张图:

状压dp↓

状压:状态压缩(把一个数组转化为一个数,从而存在状态里面)。

T1一个平面上有n个点,坐标为(x,y),从一号点出发把所有点走一遍,是距离之和最小。

分析维度:变化的量有:走到哪里了、哪些点已经走过了、当前距离。所以f[j][i]表示走到i点已经走过点j时当前的距离,但此时j是一个数组,就需要用二进制状态压缩把每一个点是否走过记录下来,走过记1,否则记0,如:100010。复杂度(n^2*2n),n的范围需要小于18。

代码:

#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
const int maxn=1001;
double f[1<<maxn][maxn];
int n,x[maxn],y[maxn];
double dis(int j,int k){
	double dx=x[j]-x[k];
	double dy=y[j]-y[k];
	return sqrt(dx*dx+dy*dy);
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>x[i]>>y[i];
	} 
	for(int i=0;i<(1<<n);i++){
		for(int j=0;j<n;j++){
			f[i][j]=1e20;
		}
	}
	f[1][0]=0;
	for(int i=0;i<(1<<n);i++){
		for(int j=0;j<n;j++){
			if(i>>j&1)//i的二进制第j位是1才是合法的状态
			{
				for(int k=0;k<n;k++){
					if((i>>k&1)==0){
						f[i|(1<<k)][k]=min(f[i|(1<<k)][k],f[i][j]+dis(j,k));
					}
				}	
			}   
		}
	}
	double ans = 1e20;
    for(int j = 1; j < n; j++) {
        ans = min(ans, f[(1<<n)-1][j] + dis(j, 0));
    }
    printf("%.2lld",ans);
	return 0;
}

T2N×N 的棋盘上放置 K 个国王,要求它们互不攻击(即任意两个国王不能相邻,包括上下左右和对角线方向)。求合法的摆放方案数。

分析维度:变量:放了几个国王和方案数。方法有两种:F1一个一个格子放;F2一行一行放。先说F2现在多一个变量:放在第几行了,也要知道上一行哪里有国王,也要记录上一行哪里有国王。f[i][j][s]代表1~i行已经放完,总共放了j个国王,s代表第i行哪些位置有国王(使用状态压缩)。枚举第i+1行如何放国王。复杂度O(nk2^(2n)),代码如下:

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
const int maxn=1e2;
int n,k;
int f[maxn][maxn][maxn];
signed main(){
	cin>>n>>k;
	f[0][0][0]=1;
	for(int i=0;i<n;i++){
		for(int j=0;j<=k;j++){
			for(int s=0;s<(1<<n);s++){
				if(f[i][j][s]){
					for(int r=0;r<(1<<n);r++){
						if(r&s) continue;
						if((r>>1)&s) continue;
						if((r<<1)&s) continue;
						if((r<<1)&r) continue;
						f[i+1][j+__builtin_popcount(r)][r]+=f[i][j][s];
					}
				}
			}
		}
	}
	long long ans=0;
    for(int s=0;s<(1<<n);s++){
        ans += f[n][k][s];
    }
    cout<<ans<<endl;
	return 0;
}

再说F1,我们也要有顺序,从第一行开始往下放。但这样做有一个好处,再放这一个格子时,只用考虑放或不放。f[i][j][k][s]表示放到(i,j),放了k个国王,轮廓线上放或未放国王,f[i][j][k][s]=方案数,应状压s。代码如下:

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
const int maxn=10;
int f[maxn][maxn][maxn][maxn],n,k;//f[i][j][k][s] 
int main()
{
	cin >> n >> k;
	f[1][0][0][0] = 1;
	//O(n^2k2^(n+1))
	for (int i=1;i<=n;i++)
		for (int j=1;j<=n;j++)//要在第i行第j列这个格子放国王
		{
			for (int r=0;r<=k;r++)
				for (int s=0;s<(1<<(n+1));s++)
					if (f[i][j-1][r][s])//方案数不为0
					{
						//ij不放国王
						f[i][j][r][(s|(1<<(j-1)))^(1<<(j-1))] += f[i][j-1][r][s];
						//ij放国王 
						if (j>=2 && (s>>(j-2)&1)==1) continue;
						if (j>=1 && (s>>(j-1)&1)==1) continue;
						if ((s>>j&1)==1) continue;
						if ((s>>(j+1)&1)==1) continue;
						f[i][j][r+1][s|(1<<(j-1))] += f[i][j-1][r][s];
					} 
			for (int r=0;r<=k;r++)
				for (int s=0;s<(1<<(n+1));s++)
					f[i+1][0][r][(s & ((1<<n)-1) << 1)] = f[i][j][r][s];
		}
	long long ans=0;
    for(int s=0;s<(1<<n);s++){
        ans += f[n][0][k][s]; // 第n行第0列,放了k个国王
    }
    cout<<ans<<endl;
} 

总结:状压DP中,哪个的数据范围小就状压哪一个。

T3农场主 John 有一块 M×NM=的牧场(1≤M,N≤12),某些格子贫瘠不能种草。要求在能种草的格子上选择若干块草地,满足:1.没有两块草地相邻(上下左右方向)。2.不考虑草地总数(包括一块不选的方案)。求所有合法的种植方案

这道题只是提了一嘴,没讲。

DP优化↓

使用数据结构优化

T1给定一个长度为 N的序列,每个位置有一个目标颜色。初始时,所有位置都没有颜色。每次操作可以选择一个区间,将该区间内的所有元素设置为它们的目标颜色。设该区间内不同颜色的数量为 X,则操作的代价为 X^2。求将所有位置染成目标颜色的最小总代价。N≤5*10^4。(来源于HDU(5009))

分析:染色顺序对答案没有影响,就定义顺序为自前向后,可以分为若干块先算。定义f[i]为把1~i完成的最小代价,此时转移方程为f[i]=(f[j]+(j-i+1)^2)min,时间复杂度为O(n^2),肯定超时,我们就要使用数据结构优化。我们通过观察不难发现,最优解一定≤n^2,但是不管它怎么给数据,一定有一种方案是每次只染一块,这样代价就为1,所以其实最优解≤n。所以每一块的x≤√n,维护从i开始向前走,保证前面有j种颜色最远走到哪里(1≤j≤√n)只要可以维护好这√n种方案,时间复杂度可降至O(n√n),可以使用双指针实现,这样就不会超时了。

T2①一块地只能被种一次;②一块地可以被种多次Hja 回到老家开始种地,由于太久没有种地,所以所有地都是荒地。将每片地从荒地变成不荒地有一定的代价,但是一旦改变之后就不再是荒地了。现在 Hja 要开始 M 年的种地生活,第 i 年 Hja 可以在 ( l_i ) 到 ( r_i ) 块地上种地,并且可以获得 ( p_i ) 的收益。(注意,要种地必须整段一起种,并且这些地一定已经是不荒地),Hja 可以选择种或者不种每一年的地,问 Hja 能够获得的最大收益。其中,N、M≤1000。

分析:我们可以设f[i][s]表示在第i年有s块地不是荒地,从而去用DP暴力计算,但是时间复杂度太高了。我们可以提前把所有要用的地开荒,后面种地的顺序就没有影响,我们可以把这M年的顺序按照某种方式排序。我们就不需要记录哪些地开荒了,哪些地没开荒,接下来就要思考按照哪种方法排序。以左端点为第一关键字,右端点为第二关键字进行排序,此时是最优的。此时我们让f[i][s]代表前i年的种地已经结束了,即前i次种地已经完成,在所有开荒的地中,最右边的是j,此时的最小代价。我们有需要分情况讨论了。

①j<l_i+1

②l_i+1≤j≤r_i+1

③r_i+1≤r

完整代码:

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
const int N = 1005;
int n, m;
int a[N], sum[N], l[N], r[N], p[N], f[N][N];

int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i-1] + a[i]; // 计算前缀和
    }
    for(int i = 1; i <= m; i++) {
        cin >> l[i] >> r[i] >> p[i];
    }
    
    memset(f, -0x3f, sizeof f); // 初始化为负无穷
    f[0][0] = 0; // 初始状态
    
    for(int i = 0; i < m; i++) {
        for(int j = 0; j <= n; j++) {
            // 不种第i+1年
            f[i+1][j] = max(f[i+1][j], f[i][j]);
            
            // 种第i+1年
            if(j < l[i+1]) {
                f[i+1][r[i+1]] = max(f[i+1][r[i+1]], f[i][j] + p[i+1] - (sum[r[i+1]] - sum[l[i+1]-1]));
            }
            else if(j <= r[i+1]) {
                f[i+1][r[i+1]] = max(f[i+1][r[i+1]], f[i][j] + p[i+1] - (sum[r[i+1]] - sum[j]));
            }
            else {
                f[i+1][j] = max(f[i+1][j], f[i][j] + p[i+1]);
            }
        }
    }
    
    int ans = 0;
    for(int j = 0; j <= n; j++) {
        ans = max(ans, f[m][j]);
    }
    cout << ans << endl;
    return 0;
}

但如果M、N≤2*10^5,那就超时了,因为时间复杂度O(mn)。此时我们可以用滚动,

①j<l_i+1

②l_i+1≤j≤r_i+1

③r_i+1≤r

T3CF193D

我们看到这道题后先想到暴力,但时间复杂度为O(n^5),直接超时了。我们就可以使用数据结构优化。(思路图没截到 qwq)

数位dp

核心:在l~r满足条件的数有多少个或还是多少

T1 f(123)=1+2+3=6 求f(l)+…………+f(r)的值。(1≤l,r≤n)

我们要从第n位开始填充数字。

代码:

#include <bits/stdc++.h>
using namespace std;

int a[20];
long long f[20][2], g[20][2];

long long dp(long long x) {
    int len = 0;
    while (x > 0) {
        a[++len] = x % 10;
        x /= 10;
    }
    memset(f, 0, sizeof(f));
    memset(g, 0, sizeof(g));
    f[len + 1][1] = 0;
    g[len + 1][1] = 1;
    for (int i = len; i >= 1; i--) {
        for (int tight = 0; tight < 2; tight++) {
            if (g[i + 1][tight]) {
                int upper = (tight == 1) ? a[i] : 9;
                for (int d = 0; d <= upper; d++) {
                    int newTight = (tight == 1) && (d == upper);
                    f[i][newTight] += f[i + 1][tight] + g[i + 1][tight] * d;
                    g[i][newTight] += g[i + 1][tight];
                }
            }
        }
    }
    return f[1][0] + f[1][1];
}

int main() {
    long long l, r;
    cin >> l >> r;
    cout << dp(r) - dp(l - 1) << endl;
    return 0;
}

T2P2657

诀窍:多一个变量就多一个维度。

与上一题相似,就不写分析了,直接上代码。

#include <bits/stdc++.h>
using namespace std;

int a[20];
long long f[20][2][10]; // f[pos][tight][last]

long long dp(long long x) {
    int n = 0;
    while (x > 0) {
        a[++n] = x % 10;
        x /= 10;
    }
    memset(f, 0, sizeof(f));
    f[n + 1][1][0] = 1; 
    for (int i = n; i >= 1; i--) {
        for (int j = 0; j < 2; j++) {
            for (int k = 0; k < 10; k++) {
                if (f[i + 1][j][k]) {
                    int up= 9;
                    if(j==1) up=a[i];
                    for (int d = 0; d <= up; d++) {
                        if (k == 0) { 
                            if (d == 0) continue; 
                            int newTight = (j == 1) && (d == up);
                            f[i][newTight][d] += f[i + 1][j][k];
                        } else { 
                            if (abs(d - k) < 2) continue;
                            int newTight = (j == 1) && (d == up);
                            f[i][newTight][d] += f[i + 1][j][k];
                        }
                    }
                }
            }
        }
    }
    long long ans = 0;
    for (int tight = 0; tight < 2; tight++) {
        for (int last = 0; last < 10; last++) {
            ans += f[1][tight][last];
        }
    }
    return ans;
}

int main() {
    long long l, r;
    cin >> l >> r;
    cout << dp(r) - dp(l - 1) << endl;
    return 0;
}

后语↓

回家要把题做完,并不要求急性地做,可以慢慢做。暑假是最好的时间,ppt上有题可以交到这个网站 。要想要拿到高分,要尽量多拿分,不丢分,做题时尽量一次过,多给自己点时间检查。

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务