做题小结-含做不来的计数DP

第一个题

首先这个题 我没做出来

我在这里还是要总结下基环树喜欢考什么
我以前做过一个交互题 题目大意忘了
反正考的是基环树最重要的一个性质 两个点之间一定存在两个走法,一个是正常走 另一个是走环

反正就是一定有两条路过来

那这个题就是考虑这个性质
还有就是正常树的 要找到>=1的这个路径很明显 任何两个点之间都行 就是
Cn2 ,那很明显答案就是那些环上的可以*2 别的其实都是 1 ,于是我们大不了把所有人都认为是2 然后减去不是环的子树就行了 非常巧妙
我们使用tarjan求出那个环 然后对环上的每一个点进行dfs 求得子树 即可 此题是一个很有意思的题目

点击查看代码
#include <bits/stdc++.h>
#define int long long 
#define endl '\n'
//必须写题解 就是基环树的两种走法的那个知识
//写tarjan坏了一定要注意
//是不是 你h数组没有初始化或者又变成0了?(-1)的时候
using namespace std;
#define debug cout<<endl<<"----------"<<endl;
const int range=3e5+100;
int n,m,a,b;
vector<int> v[range]; 
int dfn[range],low[range],tot;
int stk[range],instk[range],top;
int scc[range],siz[range],cnt;	
int ne[range*2];
int h[range*2];
int e[range*2];
int flag;
int idx;
void add(int a,int b)
{
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
void cclear()
{
	top=0;cnt=0;idx=0;tot=0;
	flag=0;
	for(int i=0;i<=n;i++)h[i]=-1;
	for(int i=1;i<=n;i++)
	{   v[i].clear();
		siz[i]=0;
		instk[i]=ne[i]=e[i]=stk[i]=scc[i]=dfn[i]=low[i]=0;
	}	
}
void tarjan(int x,int fa)
{
	dfn[x]=low[x]=++tot;
	stk[++top]=x;
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int j=e[i];
		if(!dfn[j]){
			tarjan(j,i);
			low[x]=min(low[x],low[j]);
		}
		else if(i!=(fa^1)){
			low[x]=min(low[x],dfn[j]);
		}		
	}
	if(dfn[x]==low[x])
	{
		++cnt;
		int y;
		do{
			y=stk[top--];
			v[cnt].push_back(y);
			scc[y]=cnt;
			++siz[cnt];
		}while(y!=x);
	}
}
int  dfs(int x,int fa)
{
	int g=0;
	g++;
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int j=e[i];
		if(j==fa||scc[j]==flag)continue;
		g+=dfs(j,x);		
	}
	return g;
}
void solve(){
	cin>>n;
	cclear();
	int ans=n*(n-1);
	for(int i=1,x,y;i<=n;i++)
	{
		cin>>x>>y;
		add(x,y);add(y,x);
	}
	for(int i=1; i<=n; i++)
		if(!dfn[i]) tarjan(i,0);
	for(int i=1;i<=cnt;i++)
		if(siz[i]>1) {
			flag=i;break;
		}
	for(auto i:v[flag])
	{  
		int h=dfs(i,0);
		ans-=(h*(h-1)/2);
	}	
	cout<<ans<<endl;
	cclear();
}
signed main()
{
	int t;
	cin>>t;
	while(t--)
		solve();
}

第二题

这题4月做的 重新写了一遍
是一个线性dp的好题 噢,我昨天晚上做了一个计数dp的题目 做了很久 看题解看来一个多小时还是没看懂 于是这题被我扔了

题目

回到这里
这题要怎么写?
首先得明确知道这个只好用dp去写了
如果写呢?
注意到数据1e9 可以发现最多用30把坏钥匙 再用了都是0

我们可以发现可以用二维数组记录钥匙使用数量 进行转移即可 于是朴素得写法就是dp[2e5][30]这样就行 然后转移方程式

max:
dp[i][j]=dp[i-1][j-1]+a[i]>>j
dp[i][j]=dp[i-1][j]+a[i]>>j-k

然后就可以去做了
不过这里其实还是可以优化的 进行降维 让我们回顾一下降维的原则

为什么在01背包逆序可以做到降维呢

因为j是逆序循环的 
所以dp[j]会优先于dp[j-a[i]]更新
也就是说dp[j-a[i]]就相当于dp[i-1][j-a[i]
于是就相当于dp[i-1][j-a[i]+w[i]  

相当于用上一行的dp[j-w[i]]去更新dp[j]

dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i]+w[i])

dp[j]=max(dp[j],dp[j-a[i]+w[i])

好好对比下就明白了
所以我们对这个题进行降维书写
然后一定要注意到 当n>30时 我们一定要多加一句

	if (i >= 30)dp[i][30]
 = max(dp[i - 1][30] + 0, dp[i][30]);

为什么呢 因为你会发现 他这个30如果只是在for循环更新永远不能从dp[i-1][]30]就行更新 都是29
而在后面的那个代码

dp[i - 1][j] + (a[i] >> j) - k)

它是指此刻用的好钥匙 而我们其实是想用30多把坏的呢


	for(int i=1;i<=n;i++)
	{int maxn=0;int flag;
	  for(int j=30;j>=0;j--)
	  {
		  if(j>=1)dp[j]=max(dp[j]+(a[i]>>j)-k,dp[j-1]+(a[i]>>j));
		 else dp[j]=dp[0]+a[i]-k;
		  if(dp[j]>maxn){
			  maxn=dp[j];
			  flag=j;
		  }
	  }
	}

第三个题

这道题考的很好

首先需要仔细审题

如果两个圆舞可以通过选择第一个参与者转化为另一个圆舞,则两个圆舞没区别
我忽视了这句话
这句话是什么意思 就是 12 3 4 和2 1 3 4 是没区别的 我以为什么了你知道吗 我以为 12 3 4 的全排列都没区别。。。。
然后这其实是个圆排列 由于是圆 没有头区分 所以是n!/n=(n-1)!
然后这个题可以分成多少组就是
Cnn2*12 然后这个圆排列的方案是(n21)!2 因为有两组嘛

圆排列:

5对夫妻参加一场婚礼,他们被安排在一张10个座位的圆桌就餐,
但是操办者不知道他们之间的关系,随机安排座位,问5对夫妻恰好相邻而坐的概率是多少?

要研究圆桌排列,就必须知道它和直线排列组合的区别,
举个例子,5个人排成一排有多少种方式?同学们都知道A(5,5)=5!,但是当5个人坐成一圈时,
有多少种方式?很多同学会陷入死胡同,
其实两个题目关键区别在于直线排列时排列之前相对位置已经被确定,
但是圆桌问题时每个位置都不确定,但是这种题目我们只需要先找寻任意一人A坐下,
其余人相对位置也就确定了,
比如我们可以说一个在A左面,或者是A对 面等等,所以当5个人坐成一圈时, 有A(4,4)=4!
以上来源于百度文库

没有队头区分

第四个题

这个题我没想到是DP

你会发现很多题 我都想不到是dp
因为我不会dp
再者是这道题真的很难 四维背包dp
这个数据很小的 应该考虑dp的
好了 让我们来思考下做法吧

首先看到一般元素我们可以思考到其中一维应该表示选取元素 在看到是倍数 想到取模对吧!
于是这个四维就诞生了

这道题真的很有意思 你会发现这种不同层之间有关联的 如何进行转移呢
官方代码给了一个很有创意的办法
下面一起讲到

我们假设第三维是选取的个数c
第四维是余数r然后为了防止初始化
对于这种多维的我以后都是从0开始的 省得产生不必要的麻烦
然后考虑转移公式

我们思考下对于i,j他来源于前一个i,j-1对吧

那么它可以怎么转移呢
第一种可能 我们可以不选
ai,j这个元素 那么
转移中可以这样写
dp[i][j][c][r]=dp[i][j-1][c][r]
但是一定要注意了 对于dp[i][j][c][r]而言 他不仅仅是只由dp[i][j-1][c][r]转移过来的 比如说此时的c是2表示选了两个元素
那我们由j-1的选了2个的元素转过来了 也可以是j-1-1-1的选了两个元素转移过来 当然了这里都是一样的

所以这里是要取max处理的

我当时这里琢磨了半天

我是对代码每一个细节都不许放过的!

然后我们再思考 如果要用这个元素的

我们该怎么推导这个状态转移方程呢

很明显一旦选取了元素 那么注定会导致余数的改变
int t=r+ai,j%k
转移方程应该是这样的
我们该思考转移前这二者的关系
dp[i][j][c+1][t]

dp[i][j-1][c][r]+ai,j
对吧
很明显 我如果选了这个元素他太是这个转移过来的
然后大家肯定会想到 我们是取max的
为什么呢 而不是直接等于他呢
因为dp[i][j-1][c+1][t]可能也很大 我们还是要做一个对比的 如果很大的话就取他了
然后我们可以得出(就在我写这话的时候我还是错误的理解,突然开窍了,也可以看出我做这个题也不是全懂的 这也是写题解的好处)

这个c是表示整行的选取!表示本行已经选取了c个元素了,不是这个人做第c个选取(这个人做第c个的结尾)的意思,我之前一直这么认为!

那么我们总结得出
dp[i][j][c+1][t]=max(dp[i][j-1][c][r]+ai,j,dp[i][j-1][c+1][t])

写错了,知道哪里吗

dp[i][j-1][c+1][t]

这里错了 为什么?

我们是说来到j的时候表示现在取了c个 但是你能不能保证之前的j-1没有取到c+1个吗 并且余数还不等于t?对吧

然后供上我的错解

我是这么随便的认为可能
整个for循环0-k-1
+ai,j会导致余数重复为某个值了 实际上是不会的
ai,j+x=t 那这个t是不会说
会重复的
简单证明下

(0+x)%k
(1+x)%k
....
会重复吗?不会 只是所有人都+了个x%k而已 你可以理解为整体右移动!

所以说我的理解是站不住脚的

所以这里是
dp[i][j][c+1][t]=max(dp[i][j-1][c][r]+ai,j,dp[i][j][c+1][t]

你以为就结束了吗?
不是的
终于写完了----单行的情况了

接下来介绍本题的trick

int newi = (j == m - 1 ? i + 1 : i);
int newj = (j == m - 1 ? 0 : j + 1);

然后在不选这个元素的时候
进行分类讨论
如果此时是最后一个元素了 j为m
那么不选的话
对于下一行的第一个元素选0个就有影响的 当然了第二个不选可以由前一个推导 所以就不用想什么我下一行的第二行怎么也不初始化下这种想法了

所以有
dp[i+1][0][0][r]=max(dp[i+1][0][0][r],dp[i][m-1][c][r])
m-1就是m的意思 只是从0开始了
然后为什么取max 就是m-1这个for循环接下来还有两层r,c嘛 我们要取最大的

然后再来考虑取的时候换行如何书写
首先
int t=(ai,j+r)%k

dp[i+1][0][0][t]
dp[i][m-1][c][r]+a[i][j],dp[i+1][0][0][t]

点击查看代码
#include <bits/stdc++.h>

#define endl '\n'
#define debug cout<<endl<<"----------"<<endl;
using namespace std;
const int range = 3e5 + 10;
int n;
int m;
int k;
int a[100][100];
int dp[80][80][80][80];
//你妈的 怎么这么难 卧槽卧槽我草草草草
// 卧槽卧槽我草草草草
// 卧槽卧槽我草草草草
void solve() {
	cin >> n >> m >> k;
	//做dp题 日后必须从0开始读
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			cin >> a[i][j];
	memset(dp, -1, sizeof dp);
	dp[0][0][0][0] = 0;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++) {
			for (int c = 0; c < m / 2 + 1; c++) {
				for (int r = 0; r < k; r++) {
					if (dp[i][j][c][r] == -1)continue;
					int newi = (j == m - 1 ? i + 1 : i);
					int newj = (j == m - 1 ? 0 : j + 1);
					if (newi != i) {
						dp[i + 1][0][0][r] = max(dp[i + 1][0][0][r], dp[i][m - 1][c][r]);
					} else {
						dp[i][j + 1][c][r] = max(dp[i][j + 1][c][r], dp[i][j][c][r]);
					}
					if (c <= m / 2 - 1) {
						int w = (r + a[i][j]) % k;
						if (newi != i) {
							dp[i + 1][0][0][w] = max(dp[i + 1][0][0][w], dp[i][m - 1][c][r] + a[i][j]);
						} else {
							dp[i][j + 1][c + 1][w] = max(dp[i][j + 1][c + 1][w], dp[i][j][c][r] + a[i][j]);
						}
					}
				}
			}
		}
	}
	cout << dp[n][0][0][0] << endl;
}
signed main() {
	ios::sync_with_stdio();
	cin.tie(0);
	cout.tie(0);
	solve();
	return 0;
}
终于写完了 你以为就结束了吗 没有

这题还有记忆化搜索的写法
我们还要补充这题的记忆化搜索写法

记忆化写起来非常简单

真的很简单

	if (x == n ) {
		if(r==0)return 0;
		else return -1e9;
	}

这个注意返回1e9 是因为对于那些最终没有用的路线 我们不能让他认为有用 直接返回-1e9让他取不了max就行了

点击查看代码
#include <bits/stdc++.h>
#define endl '\n'
#define debug cout<<endl<<"----------"<<endl;
using namespace std;
const int range = 3e5 + 10;
int n;
int m;
int k;
int a[100][100];
int dp[80][80][80][80];
int cal(int x, int y, int c, int r) {
	if (x == n ) {
		if(r==0)return 0;
		else return -1e9;
	}
	if (y == m || c == m / 2) {
		return cal(x + 1, 0, 0, r);
	}
	if(dp[x][y][c][r]!=-1){
		return dp[x][y][c][r];
	} 
	dp[x][y][c][r]=max(cal(x,y+1,c,r),cal(x,y+1,c+1,(r+a[x][y])%k)+a[x][y]);
//	debug
	//cout<<dp[x][y][c][r]<<endl;
//	cout<<x<<" "<<y<<" "<<c<<" "<<r<<endl;
	return dp[x][y][c][r];
}
void solve() {
	cin >> n >> m >> k;
	memset(dp,-1,sizeof dp);
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++)cin >> a[i][j];
	}
	cout << cal(0, 0, 0, 0);
}
signed main() {
	ios::sync_with_stdio();
	cin.tie(0);
	cout.tie(0);
	solve();
	return 0;
}

别的就没什么好说的了 记忆化真是yyds

第五题

这道题 有意思
我一开始写了个标准错解
我只考虑吧删除了那个出现次数*边权最大的那个边
注意我这里错在了 我只找了出现的边 没有考虑那些没走的边 可能由于边权太大 跑最短路 没有加进去

写完了 我还窃喜 这么简单
是自己太单纯了 被出题人玩了

可怜的汤姆

所以对这种情况的避免就是暴力枚举所有边

我为什么没想到暴力枚举
因为我把他看成最多有Cn2个边了 于是不敢枚举了 草

然后暴力枚举即可

这边注意枚举的转移方程

假设u-v这条边
我们i,j可能是i-u-v-j也可能是i-v-u-j 所有要去min!!

点击查看代码
#include <bits/stdc++.h>
#define endl '\n'
#define debug cout<<endl<<"----------"<<endl;
using namespace std;
const int range = 2e3 + 10;
int n;
int m;
pair<int, int>p[range * 10];
pair<int, int>h[range * 10];
bool vis[range][range];
int a[range];
int dis[range][range];
int bian[1100][1100];
struct node {
	int v;
	int w;
};
int k;
map<pair<int, int>, int>ma;
struct Node {
	int dis;
	int u;
	friend bool operator<(Node x, Node y) {
		return x.dis > y.dis;
	}
};
vector<node>e[range];
int prefix[range];
//一个人的时候 偷偷看你的微博
void dijkstra(int st) {
	for (int i = 0; i <= n; i++) {
		vis[st][i] = 0;
		dis[st][i] = 1e8;
	}
	dis[st][st] = 0;
	priority_queue<Node>q;
	q.push({0, st});
	while (q.size()) {
		auto x = q.top();
		q.pop();
		int u = x.u;
		if (vis[st][u])continue;
//		if (u == fin)continue;
		vis[st][u] = 1;
		for (auto i : e[u]) {
			int w = i.w;;
			int v = i.v;
			if (dis[st][v] > dis[st][u] + w) {
				dis[st][v] = dis[st][u] + w;
				q.push({dis[st][v], v});
//				prefix[v] = u;
			}
		}
	}
	return ;
}
void solve() {
	cin >> n >> m >> k;
	for (int i = 1, x, y, w; i <= m; i++) {
		cin >> x >> y >> w;
		e[x].push_back({y, w});
		e[y].push_back({x, w});
		h[i].first = x;
		h[i].second = y;
//	     	bian[x][y]=w;
//			bian[y][x]=w;
		//题目保证一条 多条也没啥 大不了预处理min下
	}
	for (int i = 1; i <= k; i++) {
		cin >> p[i].first >> p[i].second;
	}
	for (int i = 1; i <= n; i++)
		dijkstra(i);
	//m=min(cn2,1000) 不是max!
	int maxn=1e9;
	for (int i = 1; i <= m; i++) {
		int x = h[i].first;
		int y = h[i].second;
		//x->y这条边
		int ans=0;
		for (int j = 1; j <= k; j++) {
			int st = p[j].first;
			int fin = p[j].second;
			int xx= min(dis[st][fin], min(dis[st][x] + dis[y][fin], dis[st][y] + dis[x][fin]));
			ans+=xx;	
		}
	//	cout<<ans<<endl;
		maxn=min(ans,maxn);
	}
	cout<<maxn<<endl;
}
signed main() {
	ios::sync_with_stdio();
	cin.tie(0);
	cout.tie(0);
	solve();
	return 0;
}
//	int step=0;
//	for (int i = 1, x, y; i <= k; i++) {
//		cin >> x >> y;
//		dijkstra(x, y);
//		while (prefix[y]) {
//           int u=prefix[y];
//			p[++step]={u,y};//u->y
//			y=u;
//		}
//	}
//	int maxn=0;
//	int sum=0;
//	for(int i=1;i<=step;i++)
//	{
//		int x=p[i].first;
//		int y=p[i].second;
//		int spend=bian[x][y];
//		ma[{x,y}]+=spend;
//		sum+=spend;
//		maxn=max(ma[{x,y}],maxn);
//	}
//	cout<<sum-maxn<<endl;

第六题

写的我好累啊

好了
这道题
我做的时候把正确做法叉掉了
服自己了

一定要注意把握自己那灵光一现的灵感或者说是别的自己想法这些 一定要珍惜他们 要做到写在草稿纸至少证明出错误
最后一道题 也会解释这有多么重要

这道题很明显以前做过

然后思考到某个数字出现第二遍说明我们有0了此时ans++ 然后clear掉我们的缓存 然后要注意到此题与之前做到的不同 之前没有加入插入一个数这种说法 所以那个题都不要clear 但这个就不同了 我们插入了 就一定保证后面的数跟着我前面这个序列的一定不会为0因为我插入无限大的数进来

所以 我需要让sum=a[i]而不是sum=0或者不做处理
别的就没啥了

点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
#define debug cout<<endl<<"----------"<<endl;
using namespace std;
const int range = 3e5 + 10;
int n;
int a[range];
void solve() {
	//出现这种情况 怎么办? 老是把正解自己hack掉 不敢坚持写下去
	//一定要拿笔仔细思考下 别老是hack自己
	//为什么自己老是干这种事情 笨比
	map<int, int>ma;
	cin >> n;
	int sum = 0;
	//1 -1 1 
	// 
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	int ans = 0;ma[0]=1;
	for (int i = 1; i <= n; i++) {
		sum += a[i];
		if (ma[sum]) {
		//	cout<<i<<" "<<sum<<endl;
//	          sum=0;
			ans++;
			sum=a[i];//i-1前收到影响
			ma.clear();
			ma[0]=1;
		}
		ma[sum]++;
	}
	//-6 -1 1 -6 12 
	//不能清空 可以看出 
	cout << ans << endl;
}
signed main() {
	ios::sync_with_stdio();
	cin.tie(0);
	cout.tie(0);
	solve();
	return 0;


}

最后一道

这道题的做法说实话我不会证明
我也不知道为什么就ac了
我只是抓住了我灵感一现的想法

很明显求最小的 我肯定这么思考假设我是alice 我怎么最小呢
那很明显 让我最大的那个浮上来去跟别人抵消 抵消完了就完了 剩下的就加答案上去

想法很简单的就是这样的

然后你会发现剪刀碰上剪刀和石头才不算赢 那我到底怎么合理组织先碰哪个呢 答案是谁大先消耗谁 因为我当时贪心的想谁大耗谁 毕竟你比较大 你用光了 那你的对位我可以帮你补 这样也是可以的

然后举个例子
此时剪刀x
有剪刀y 石头z 但是y>>z那么

我们思考y的天敌是石头z可以覆盖
y的抵消是石头 剪刀 z对他没有负影响 并且y用多了还防止了答案变多的可能性 于是派最多的上还是有点道理的 我就是这么想的

AC了的代码就是好代码

正解是n-min(a1,n-b2)-min(a2,n-b3)-min(a3,n-b1)
如此简单 没想到。。。
最服的是
还有人写网络流。。。
不过扶苏都没想到这个结论耶

未解决难题

一道计数dp 我没做出来 题解也没看懂荒废了几个小时 希望将来来写掉它

噫吁嚱,终于写完了

posted @   想念不动声色  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示