2022-02-21 ~ 2022-02-28 周总结

课程好多啊,准备熬夜肝完...
加油!拿下PAT顶级这一次!

2022-02-21

1023 The Best Polygon

首先题目给我们的一定是一个凸多边形吧,
我们首先获得这些点中的较为平均的点,按照这个平均点进行极角序排序,然后走dp
dp[i][j][k]表示起始点为i,终点为j,多边形的个数时k的最大面积
那么dp[i][j][k] = max(dp[i][l][k - 1] + Area(i,j,l))其中i ~ l中的点数需要大于k - 1,Area指的是由i,j,l组成的三角形面积
AC代码:

点击查看代码
#include <iostream>
#include <algorithm> 
#include <vector>
#include <cmath>
using namespace std;
const int MAXN = 3e2 + 7;
struct point{
	double x,y;
	int idx;
	point(double x = 0,double y = 0):x(x),y(y){}
}a[MAXN];
double Mx,My;
double cross(point a,point b)
{return a.x * b.y - a.y * b.x;}
bool cmpp(point a,point b)
{
    // OA , OB 向量
    double oax = a.x - Mx;
    double oay = a.y - My;
    double obx = b.x - Mx;
    double oby = b.y - My;
    return atan2(oay,oax) < atan2(oby, obx);
}
bool cmp(point a,point b)
{return cross(point(a.x - Mx,a.y - My),point(b.x - Mx,b.y - My)) < 0;}
double dp[MAXN][MAXN][15];
int pre[MAXN][MAXN][15];
double Area(point a,point b,point c)
{return fabs(cross(point(a.x - b.x,a.y - b.y),point(c.x - b.x,c.y - b.y))) / 2.0;}
int main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i <= n;++i)
	{
		scanf("%lf %lf",&a[i].x,&a[i].y);
		Mx += a[i].x;
		My += a[i].y;
		a[i].idx = i;
	}
	Mx /= n;
	My /= n;
	sort(a + 1,a + n + 1,cmp);
	
	for(int k = 3;k <= m;++k)
	{
		for(int i = 1;i + k - 1 <= n;++i)
		{//枚举起点 
			for(int j = i + k - 1;j <= n;++j)
			{
				for(int l = i + k - 2;l <= j - 1;++l)
				{
					double now = Area(a[i],a[j],a[l]) + dp[i][l][k - 1];
					if(now > dp[i][j][k])
					{
						dp[i][j][k] = now;
						pre[i][j][k] = l;
					}
				}
			}
		}
	}
	double ans = 0;
	int ansi = -1,ansj = -1;
	for(int i = 1;i <= n;++i)
	for(int j = 1;j <= n;++j)
	{
		if(ans < dp[i][j][m])
		{
			ans = dp[i][j][m];
			ansi = i;
			ansj = j;
		}
	}
//	cout << dp[ansi][ansj][m] << endl;
	vector<int> res;
	int k = m;
	while(ansj)
	{
//		printf(">>%d\n",a[ansj].idx);
		res.push_back(a[ansj].idx);
		ansj = pre[ansi][ansj][k];
		k -= 1;
	}
	res.push_back(a[ansi].idx);
	sort(res.begin(),res.end());
	reverse(res.begin(),res.end());
	for(int i = 0;i < m;++i)
	printf("%d%c",res[i] - 1," \n"[i == m - 1]);
	return 0;
}
这道题和一道经典的区间DP很像:Loj10149多边形划分 但是这一题的所选的个数是限定住的,最好还是按照k - 1的形式进行转移 ↑蒟蒻的猜想,欢迎大佬指正,Orz

[AHOI2009]CHESS 中国象棋

首先考虑的是如果n和m都是小于8的。
这时候我们会考虑使用状态压缩来求,设第i行的状态为s,所选的列有几个1进行转移。
但是这个n,m最大可以到100?这样怎么求解呢?
考虑优化,我们知道具体哪一列有1很重要吗?显然不是,由于一行符合要求的最多就只有2个1,那么我们直接把这个加入状态即可:
设dp[i][j][k]表示前i行,有j个列没有1,k个列有1个1,m - j - k个列有2个1的符合要求的摆放个数
然后就可以做如下的转移:

第i列放一个
1、放在那j列中
k >= 1
dp[i][j][k] += dp[i - 1][j + 1][k - 1] * (j + 1);
2、放在那k列中
m - j - k >= 1
dp[i][j][k] += dp[i - 1][j][k + 1] * (k + 1);

第i列放了2个
1、都放在那没放的列中
k >= 2
dp[i][j][k] += dp[i - 1][j + 2][k - 2] * (j * (j - 1))
2、都放在放了一个的列中
m - j - k >= 2
dp[i][j][k] += dp[i - 1][j][k + 2] * (k * (k - 1))
3、一个放在了无球的列中,一个放在了有一个球的列中
m - j - k >= 1
dp[i][j][k] += dp[i - 1][j + 1][k] * (2 * (j + 1) * (k + 1))

AC代码:

点击查看代码
#include <iostream>
using namespace std;
const int MAXN = 1e2 + 7;
const int MOD = 9999973;
typedef long long ll;
ll dp[MAXN][MAXN][MAXN];//前i行 有j列是啥也没放的,k列是放了一个的 m - j - k是放了两个的
/*
第i列放一个
1、放在那j列中 
k >= 1
dp[i][j][k] += dp[i - 1][j + 1][k - 1] * (j + 1);
2、放在那k列中
m - j - k >= 1
dp[i][j][k] += dp[i - 1][j][k + 1] * (k + 1);
 
第i列放了2个
1、都放在那没放的列中 
k >= 2 
dp[i][j][k] += dp[i - 1][j + 2][k - 2] * (j * (j - 1))
2、都放在放了一个的列中
m - j - k >= 2
dp[i][j][k] += dp[i - 1][j][k + 2] * (k * (k - 1))
3、一个放在了无球的列中,一个放在了有一个球的列中
m - j - k >= 1
dp[i][j][k] += dp[i - 1][j + 1][k] * (2 * (j + 1) * (k + 1)) 
*/
/*
50 12
*/
int main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	dp[0][m][0] = 1;
	
	for(int i = 1;i <= n;++i)
	{
		for(int j = 0;j <= m;++j)
		for(int k = 0;k + j <= m;++k)
		{
			int p = m - j - k;
			dp[i][j][k] += dp[i - 1][j][k];
			if(k >= 1)
			dp[i][j][k] += dp[i - 1][j + 1][k - 1] * (j + 1) % MOD;
			dp[i][j][k] %= MOD;
			
			if(p >= 1)
			dp[i][j][k] += dp[i - 1][j][k + 1] * (k + 1) % MOD;
			dp[i][j][k] %= MOD;
			
			if(k >= 2)
			dp[i][j][k] += dp[i - 1][j + 2][k - 2] * ((j + 2) * (j + 1) / 2) % MOD;
			dp[i][j][k] %= MOD;
			
			if(p >= 2)
			dp[i][j][k] += dp[i - 1][j][k + 2] * ((k + 2) * (k + 1) / 2) % MOD;
			dp[i][j][k] %= MOD;
			
			if(p >= 1 && k >= 1)
			dp[i][j][k] += dp[i - 1][j + 1][k] * ((j + 1) * k) % MOD;
			dp[i][j][k] %= MOD;
		}
	}
	ll ans = 0;
	for(int i = 0;i <= m;++i)
	for(int j = 0;j + i <= m;++j)
	ans = (ans + dp[n][i][j]) % MOD;
	
	printf("%lld\n",ans);
}

区间价值

考虑如果已知了长度为i - 1的区间价值之后,怎么推导长度为i的区间价值?
看个例子稍微理解理解:
1 2 4 2 2
长度为2的区间
1 2、2 4、4 2、2 2
答案:
2、2、2、1
长度为3的区间
1 2 4、2 4 2、4 2 2
答案
3、2、2
可以看见
F[1 2 4] = F[1 2] + 1;
F[2 4 2] = F[2 4] + 0;
F[4 2 2] = F[4 2] + 0;
最后还要减去最后一个长度为i - 1区间的那个答案
那么我们的转移方程可以写成这样:
dp[i] = dp[i + 1] + G[i] - dif[i - 1];
考虑G[i]怎么求:
观察上诉例子,可以发现会影响G[i]的只会和下标为3 4 5的值有关
可以找见,如果上诉下标所代表的值上一次出现的位置和当前位置的距离差>=i,那么就会对答案贡献一个1
即G[i] = 长度为n的序列中相同大小数字长度 >= i的二元组出现的个数
这个我们可以一遍O(n)求长度为i的二元组有多少个,然后对其求一个后缀和即可

对于dif[i],从序列的最后一个元素开始往前扫,统计其后缀区间价值即可
AC代码:

点击查看代码
#include <iostream>
using namespace std;
const int MAXN = 1e6 + 7;
int a[MAXN];
typedef long long ll;
ll dp[MAXN];
ll pre[MAXN],det[MAXN],dif[MAXN];
bool vis[MAXN];
int main()
{
	int n;
	scanf("%d",&n);
	for(int i = 1;i <= n;++i)	scanf("%d",&a[i]);
	
	for(int i = 1;i <= n;++i)
	{
		det[i - pre[a[i]]] += 1;
		pre[a[i]] = i;
	}
	for(int i = n;i >= 1;--i)	det[i] = det[i + 1] + det[i];
	
	int cnt = 0;
	for(int i = n;i >= 1;--i)
	{
		if(!vis[a[i]])
		{
			cnt += 1;
			vis[a[i]] = 1;
		}
		dif[n - i + 1] = cnt;
	}
	
	dp[0] = 0;
	for(int i = 1;i <= n;++i)
	dp[i] = dp[i - 1] + det[i] - dif[i - 1];
	
	int q;
	scanf("%d",&q);
	while(q--)
	{
		int x;
		scanf("%d",&x);
		printf("%lld\n",dp[x]);
	}
	return 0;
}
不得不说,作为菜鸡的我只能夸赞:这也太巧妙了8

排列游戏

由于只具有三种情况0 1 2,0的情况是随意的,我们看一下1 和 2对答案会产生什么影响
1 : a[i] < a[i + 1]
2 : a[i] > a[i + 1]
那么其实当前第i位其实之和前一位有关,而且和前一位具体是什么数字有关
设dp[i][j]代表前i位数字,当前第i位填的是j的方案数
如果当前的约束是0:
dp[i][j] = sum(dp[i - 1][k]) k ∈ [1,i - 1]
因为我想填什么数在第i - 1位都是可行的
约束是1:
dp[i][j] = sum(dp[i - 1][k]) k ∈ [1,j - 1]
因为前一位要小于j
约束是2:
dp[i][j] = sum(dp[i - 1][k]) k ∈ [j - 1,i - 1]
因为j要小于前一位;
很多人这时候有个疑问,上诉区间左界为什么是j - 1而不是j
我在这就做一个简单的感性理解吧...(555
因为前i - 1位的数字已经构成了一个排列,现在是多加入了一个数字i
在第i位添加一个数字j;
相当于前i位中的数字 > j位置上的数字都增加1(这样好让i加入前i - 1个位置当中,显然这时候少了一个j,前i - 1个位置上的数还是能保持和i - 1的排列一样的结果...)
因此反过来就相当于第i位添加的数是j - 1,因此左界是j - 1
AC代码:

点击查看代码
#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
const int MAXN = 5e3 + 7;
const int MOD = 998244353;
ll dp[MAXN][MAXN],su[MAXN];
char s[MAXN];
/*
000000
*/
int main()
{
	scanf(" %s",s + 1);
	int n = strlen(s + 1);
	dp[1][1] = 1;
	
	su[1] = 1;
	for(int i = 2;i <= n + 1;++i)
	{
		for(int j = 1;j <= i;++j)
		if(s[i - 1] == '0')
		dp[i][j] = su[i - 1];
		else if(s[i - 1] == '1')
		dp[i][j] = su[j - 1];
		else	dp[i][j] = (su[i - 1] - su[j - 1] + MOD) % MOD;
		
		for(int j = 1;j <= i;++j)
		su[j] = (su[j - 1] + dp[i][j]) % MOD;
	}
	ll ans = su[n + 1];
//	for(int i = 1;i <= n + 1;++i)
//	(ans += dp[n + 1][i]) % MOD;
	
	printf("%lld\n",ans);
	return 0;
}

[ZJOI2008]骑士

前置题目:没有上司的舞会
这道题多出来的就是对于一颗树,多了一条边,使其中存在环...
那么我们这样考虑,将该环断开,然后又得到一棵树,对断开边的两个端点来说,我们如果要拿的话,只会拿走其中的一个。
那么我们只需要对这两个端点进行树形DP。然后两个端点分别不取中取一个最大值就是答案。
这里有一个坑就是,题目中构成的基环树可能不止一个
AC代码:

点击查看代码
#include <iostream>
#include <cstring> 
#include <vector>
using namespace std;
const int MAXN = 1e6 + 7;
typedef long long ll;
const ll inf = 1e12;
struct node{
	int ne,to,w;
}a[MAXN << 2];
int head[MAXN],cnt = 0;
void add(int x,int y,int w = 0)
{
	a[++cnt].ne = head[x];
	head[x] = cnt;
	a[cnt].w = w;
	a[cnt].to = y;
}
ll A[MAXN];
bool vis[MAXN];
int fa[MAXN],root = 0;
ll dp[MAXN][2];
/*
8
1 2
1 5
1 1
1 2
1 6
1 3
1 8
1 7
*/
bool v[MAXN];
int p1,p2;
void dfs(int x,int fa)
{
	vis[x] = 1;
	for(int i = head[x];i;i = a[i].ne)
	{
		int y = a[i].to;
		if(y == fa)	continue;
		if(vis[y])
		p1 = x,p2 = y;
		else
		dfs(y,x);
	}
}
vector<int> tmp;
void go(int x,int fa)
{
	dp[x][0] = 0;
	dp[x][1] = A[x];
	
	v[x] = 1;
	tmp.push_back(x);
	for(int i = head[x];i;i = a[i].ne)
	{
		int y = a[i].to;
		if(y == fa || v[y])	
		continue;
		go(y,x);
		dp[x][1] += dp[y][0];
		dp[x][0] += max(dp[y][1],dp[y][0]);
	}
}
void clear()
{
	for(int i = 0;i < tmp.size();++i)	v[tmp[i]] = 0;
	tmp.clear();
}
ll solve(int x)
{
	ll ans = 0;
	dfs(x,-1);
	root = p1;
	go(root,-1);clear();
	ans = max(ans,dp[root][0]);
	
	root = p2;
	go(root,-1);clear();
	ans = max(ans,dp[root][0]);
	return ans;
}
/*
3
100 2
100 1
12 1
*/
int main()
{
	int n;
	scanf("%d",&n);
	for(int i = 1;i <= n;++i)
	{
		int ha;
		scanf("%lld %d",&A[i],&ha);
		fa[i] = ha;
		add(ha,i);
		add(i,ha);
	}
	ll ans = 0;
	for(int i = 1;i <= n;++i)
	if(!vis[i])
	ans += solve(i);
	
	printf("%lld\n",ans);
	return 0;
}

2022-02-22

今天和老同学出去聚了一聚(虽然只是晚上去玩的,而且写这段话的时候已经24号凌晨了...就是偷了小懒好吧)

2022-02-23

快餐店

N条双向道路的连通图...如果删去一条边的话就是一颗树咯,因此最后我们算的时候是否可以转化为一颗树或者多颗树的问题呢?
显然是可以的咯,因为多加了一个条件就是多了一条边吗,我们后面稍微做点转化就行了
我们先来考虑一下如何在一棵树上求解这个问题:
找出最佳的距离,使得离最远的顾客距离尽可能的小。首先一棵树的直径是最长的,那么最大距离尽可能的小,只能是取直径中点的咯。

然后思考多了一条边的情况:
根据画图,我们可以想到,相当于是树上,多了一个环,形成一种类似细菌的结构,我们可以看成是,一个环上的所有点都构成了一颗独立的子树。
首先对于这个图,答案选取一共有两种情况,一种是经过环的一条最长链,一种是不经过环的一条最长链。后者我们直接对环上的所有点进行上面树求直径的操作就行。
然后考虑如果经过环,我们如何求的最长的两个叶子节点的距离。
我们行先将这个环断开,例如:
1 2 3 4 5 6是环上的点,其每个子树我们可以先不考虑
只需要事先将所有环上节点到它子树的最长链的长度记为:A[i]
然后枚举断开的位置i(假设最长链不经过i - i + 1这条边)
记数组pre[i]表示环上1到i的距离
记数组suf[i]表示环上6到i的后缀距离
B1[i]为i点之前最长从1节点出发到其前子树的距离(B1[i] = max(B1[i - 1]),pre[i] + A[i])
B2[i]为i + 1点之后最长从6接地那出发到其前子树的距离(B2[i] = max(B2[i + 1],suf[i] + A[i]))
记C1[i]为前i个节点中,两个结点的子树经过环上边的最大距离(前缀)
可知其求法为:
C1[i] = max(C1[i - 1],pre[i] + A[i] - pre[j] + A[j])
C2[i]即为后缀和C1类似
最终以上四个值中取一个最大值,就是断开i - i + 1环上这条边的最大距离
由于对于外卖员视角来说,如果有近的方案,一定选择的是最近的那条边,所以对所有断边的操作所求的最大值在取个最小值就是经过环上边的这个图的最大直径
AC代码:

点击查看代码
#include <iostream>
#include <map>
#include <vector>
using namespace std;
const int MAXN = 1e5 + 7;
struct node{
	int ne,to;
	double w;
}a[MAXN << 2];
int head[MAXN],cnt = 0;
void add(int x,int y,double w = 0)
{
	a[++cnt].ne = head[x];
	head[x] = cnt;
	a[cnt].to = y;
	a[cnt].w = w;
}
int vis[MAXN],fa[MAXN];
int loop[MAXN],tot = 0,nid = 0;
void get_cir(int x,int f)
{
	vis[x] = ++nid;
	for(int i = head[x];i;i = a[i].ne)
	{
		int y = a[i].to;
		if(y == f)	continue;
		if(vis[y])
		{
			if(vis[y] < vis[x])	continue;
			loop[++tot] = y;
			for(;y != x;y = fa[y])
			loop[++tot] = fa[y];
		}
		else
		fa[y] = x,get_cir(y,x);
	}
}
double tre[MAXN],Max;
int id = 0;
vector<int> tmp;
void clear(){
	for(int i = 0;i < tmp.size();++i)	vis[tmp[i]] = 0;
	tmp.clear();
}
void dfs1(int x,int fa,double dep)
{
	if(dep > Max)
	{
		Max = dep;
		id = x;
	}
	for(int i = head[x];i;i = a[i].ne)
	{
		int y = a[i].to;
		if(y == fa || vis[y])	continue;
		tmp.push_back(y);
		dfs1(y,x,dep + a[i].w);
	}
}
map<int,double> mp[MAXN];
double pre[MAXN],suf[MAXN];
double A[MAXN],B[MAXN],C[MAXN];
double B1[MAXN],B2[MAXN],C1[MAXN],C2[MAXN]; 
int main()
{
//	freopen("in.txt","r",stdin);
	int n;
	scanf("%d",&n);
	for(int i = 1;i <= n;++i)
	{
		int x,y;
		double w;
		scanf("%d %d %lf",&x,&y,&w);
		add(x,y,w);
		add(y,x,w);
		mp[y][x] = mp[x][y] = w;
	}
	get_cir(1,-1);
	for(int i = 1;i <= n;++i)	vis[i] = 0;
	for(int i = 1;i <= tot;++i)	vis[loop[i]] = 1;
	
	double ans = 0;
	for(int i = 1;i <= tot;++i){
		Max = 0;
		id = 0;
		vis[loop[i]] = 0;
		dfs1(loop[i],-1,0);clear();
		A[i] = Max;
		vis[loop[i]] = 0;
		dfs1(id,-1,0);
		
		vis[loop[i]] = 1;
		tre[loop[i]] = Max;
		ans = max(ans,Max);
	}
	
	for(int i = 2;i <= tot;++i)
	pre[i] = pre[i - 1] + mp[loop[i - 1]][loop[i]];
	for(int i = tot - 1;i >= 1;--i)
	suf[i] = suf[i + 1] + mp[loop[i]][loop[i + 1]];
	
	for(int i = 1;i <= tot;++i)
	B1[i] = max(B1[i - 1],pre[i] + A[i]),B[i] = max(B[i - 1],A[i] - pre[i]);
	for(int i = tot;i >= 1;--i)
	B2[i] = max(B2[i + 1],suf[i] + A[i]),C[i] = max(C[i + 1],A[i] - suf[i]);
	for(int i = 1;i <= tot;++i)
	C1[i] = max(C1[i - 1],pre[i] + A[i] + B[i - 1]);
	for(int i = tot;i >= 1;--i)
	C2[i] = max(C2[i + 1],suf[i] + A[i] + C[i + 1]);
	
	double k1 = C1[tot];
	for(int i = 1;i < tot;++i)
	{
		double nowMax = 0;
		nowMax = max(nowMax,B1[i] + B2[i + 1] + mp[loop[1]][loop[tot]]);
		nowMax = max(nowMax,C1[i]);
		nowMax = max(nowMax,C2[i + 1]);
		
		k1 = min(k1,nowMax);
	}
	ans = max(ans,k1);
	printf("%.1f\n",ans / 2);
	return 0;
}

迷失游乐园

和前一题类似,也是一棵树上多了一条边。
同样的,我们思考一下如果是棵树怎么求,很显然使用树形DP+换根就可以求出所有点的路径长度期望,最后再求个平均即可。
然后它多了一条边,我们还是看这个图中那个环上回产生多少的期望贡献。
可以发现,如果走环上的点,只会向环左或者右边走,而且还不会再次经过我的出发点(显然QAQ),那么也类似是求一棵树上的期望。然而这个时候我们并不需要,进入环上各个子树挨个去求。因为如果我们已知了环上各个子树的期望之后,不是直接从环上点期望就是走子树的期望咯。
然后求概率...就是另外的数学功夫了...
Get新知识,算期望可以先不乘以概率,保留它最后再乘,这样对于换根时或者环上求期望的时候可以简化式子
AC代码:

点击查看代码
#include <iostream>
#include <cstring>
#include <vector>
#include <map>
using namespace std;
const int MAXN = 1e5 + 7;
struct node{
	int ne,to;
	double w;
}a[MAXN << 1];
map<int,double> mp[MAXN];
int head[MAXN],cnt = 0;
int loop[25],vis[MAXN],tot,id,fa[MAXN];
double E[MAXN];
void add(int x,int y,double w)
{
	a[++cnt].ne = head[x];
	head[x] = cnt;
	a[cnt].to = y;
	a[cnt].w = w;
}
void get_cir(int x,int f)
{
	vis[x] = ++id;
	for(int i = head[x];i;i = a[i].ne)
	{
		int y = a[i].to;
		if(y == f)	continue;
		if(vis[y])
		{
			if(vis[y] < vis[x])	continue;
			loop[++tot] = y;
			for(;y != x;y = fa[y])
			loop[++tot] = fa[y];
		}
		else	fa[y] = x,get_cir(y,x);
	}
}
double dp[MAXN];
int son[MAXN],deg[MAXN];
vector<int> tmp;
int root;
void dfs(int x,int fa)
{//正常求给定根的期望 
	son[x] = 0;
	dp[x] = 0;
	for(int i = head[x];i;i = a[i].ne)
	{
		int y = a[i].to;
		if(y == fa || vis[y])	continue;
//		tmp.push_back(y);
		dfs(y,x);
		E[x] += (dp[y] + a[i].w);
		son[x] += 1;
	}
	if(son[x] != 0)
	dp[x] = E[x] / (son[x]);
	if(x != root)	son[x] += 1;
}
void clear()
{
	for(int i = 0;i < tmp.size();++i)	vis[tmp[i]] = 0;
	tmp.clear();
}
void go(int x,int fa)
{//换根 
	for(int i = head[x];i;i = a[i].ne)
	{
		int y = a[i].to;
		if(y == fa || vis[y])	continue;
		double w = a[i].w;
		int k = deg[x] - 1;
		if(!k)	k += 1;
//		dp[y] = ((dp[x] * deg[x] - dp[y] - w) / k + w) / deg[y] + \
//		(dp[y] * (deg[y] - 1) / deg[y]);
		E[y] += (E[x] - dp[y] - w) / max(1,son[x] - 1) + w;
		go(y,x);
	}
}
/*
5 5
1 2 1
2 3 1
3 4 1
4 1 1
1 5 2

4 4
1 2 1
2 3 1
3 4 1
4 1 1


5 4
1 2 1
1 3 1
3 4 1
3 5 1

4 4
1 2 1
2 3 1
3 1 1
1 4 10
*/
double g[MAXN],f[MAXN];
void get(int x,int fa)
{//统计环上的期望 
    bool has_son = 0;
    g[x] = 0;
    for(int i = head[x];i;i = a[i].ne)
    {
    	int y = a[i].to;
    	if(y == fa || !vis[y] || y == root)	continue;//必须得是环上的 
    	has_son = 1;
    	get(y,x);
    	g[x] += g[y] + a[i].w;
	}
	if(x == root)	return ;
	int k = son[x];if(!k)	k += 1;
	if(!has_son)	g[x] = E[x] / k;
	else k = son[x] + 1,g[x] = (E[x] + g[x]) / k;
}
int main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i <= m;++i)
	{
		int x,y;
		double w;
		scanf("%d %d %lf",&x,&y,&w);
		add(x,y,w);
		add(y,x,w);
		deg[x] += 1;
		deg[y] += 1;
		mp[x][y] = mp[y][x] = w;
	}
	get_cir(1,-1);
	for(int i = 1;i <= n;++i)	vis[i] = 0;
	for(int i = 1;i <= tot;++i)	vis[loop[i]] = 1;
	if(tot == 0)
	{
		root = 1;
		dfs(1,-1);
		go(1,-1);
		
		for(int i = 1;i <= n;++i)	dp[i] = E[i] / deg[i];
		double ans = 0;
		for(int i = 1;i <= n;++i)	ans += dp[i];
		ans /= n;
		printf("%.5f\n",ans);
		return 0; 
	}
	
	for(int i = 1;i <= tot;++i)
	{
		root = loop[i];
		dfs(loop[i],-1);//统计子环上子树信息 
//		clear();
	}
	
//	for(int i = 1;i <= n;++i)	f[i] = dp[i];
	/* 
	for(int i = 1;i <= tot;++i)
	{
		double pk = 1.0 / deg[i],w = 0;
		dp[i] /= deg[i];
		for(int j = 1;j < tot;++j)
		{	
			int k = (i + j) % tot;
			int ls = ((k - 1) + tot) % tot;
			if(ls == 0)	ls = tot;
			if(k == 0) k = tot;
			
			w = mp[loop[ls]][loop[k]];
			dp[i] += pk * (f[k] * (son[k] - 1) / (son[k]) + w);
			pk /= son[k];
		}
		w = 0;pk = 1.0 / deg[i];
		for(int j = 1;j < tot;++j)
		{
			int k = ((i - j) + tot) % tot;
			int ls = (k + 1) % tot;
			if(ls == 0)	ls = tot;
			if(k == 0) k = tot;
			
			w = mp[loop[ls]][loop[k]];
			dp[i] += pk * (f[k] * (son[k] - 1) / (son[k]) + w);
			pk /= son[k];
		}
//		dp[i] /= deg[i];
	}
	*/
	//统计环上期望
	for(int i = 1;i <= tot;++i)
	{
		root = loop[i];
		get(loop[i],-1);
		f[loop[i]] = g[loop[i]];
	}
	for(int i = 1;i <= tot;++i)
	E[loop[i]] += f[loop[i]],son[loop[i]] += 2;
	//现在开始统计各个子树信息
	//换根DP
	for(int i = 1;i <= tot;++i)
	go(loop[i],-1);
	
	for(int i = 1;i <= n;++i)
	dp[i] = E[i] / deg[i];
	
	double ans = 0;
	for(int i = 1;i <= n;++i)	ans += dp[i];
	ans /= n;
	printf("%.5f\n",ans);
	return 0;
}

高楼实验

设计状态:
f[i][j]表示i个鸡蛋,当前在一共j层楼所需要的最少实验次数
那么在第w层扔一个鸡蛋,有两种可能:
①鸡蛋碎了
f[i][j] = f[i - 1][w - 1] + 1
只需要去1 ~ w - 1层试就行
②鸡蛋没碎
f[i][j] = f[i][j - w] + 1
只需要去w + 1 ~ j层试试就行,就可以转化为1 ~ j - w层
然后我们发现这样算的话需要枚举i j w复杂度n^3
n = 1e3,考虑优化
我们发现,其实蛋如果无穷个数的话,其实根本用不了那么多去测试,我们直接在楼层上进行二分即可,于是蛋的最大个数下降为log(m + 1) / log(2)
如果n > 上述,就直接输出上述即可
否则跑上面的算法。
现在的复杂度是n ^ 2 * log(n)可以过,但是还可以优化
观察式子f[i - 1][w - 1]和f[i][j - w]
其实楼层越多,我们需要实验的次数也就越多,那就说明一个事实,f[i][j]是单调不减的。那么对f[i][j - w]和f[i - 1][w - 1]中取一个最小值,就是求这两个函数的交叉点的位置,我们只需要找到那个位置进行转移即可。由于具有单调性,因此直接上二分即可。
复杂度进一步降为n*log(n)^2
AC代码:

点击查看代码
#include <iostream>
#include <cmath>
using namespace std;
const int MAXN = 1e3 + 7;
const int inf = 1e9;
int dp[MAXN][MAXN];
int main()
{
	int n,m;
	scanf("%d %d",&n,&m);
    if(n >= ceil(log(m + 1) / log(2)))
    {
    	cout << ceil(log(m + 1) / log(2)) << endl;
        return 0;
    }
	for(int i = 1;i <= n;++i)
	for(int j = 1;j <= m;++j)
	{
		dp[i][j] = inf;
		int le = 1,ri = j - 1,ans = ri;
		while(le <= ri)
		{
			int mid = le + ri >> 1;
			if(dp[i - 1][mid - 1] >= dp[i][j - mid])	ri = mid - 1,ans = mid;
			else	le = mid + 1;
		}
		for(int k = -5;k <= 5;++k)
		{
			int id = ans + k;
			if(id >= 1 && id <= j)
			dp[i][j] = min(max(dp[i][j - id],dp[i - 1][id - 1]) + 1,dp[i][j]);
		}
//		int Max = 1e9;
//		for(int k = 1;k < j;++k)
//		{
//			int now = max(dp[i][j - k],dp[i - 1][k - 1]);
//			Max = min(Max,now);
//		}
//		dp[i][j] = Max + 1;
//		dp[i][j] = min(max(dp[i][j - k],dp[i - 1][k - 1]) + 1,dp[i][j]);
	}
	cout << dp[n][m];
	return 0;
}

选人

构造转移方程
f[i][j]表示前i个人选了j组的最大战力之和
转移:
f[i][j] = max(f[k][j - 1],f[i - 1][j]) + a[j];
k∈[1,i - 1]
由于n的范围比较大,考虑优化
我们观察式子中的j,之和前一个j - 1的所有情况和当前的i - 1的情况有关。
因此我们优先枚举求j,对于f[k][j - 1]只需要求前缀最大即可
AC代码:

点击查看代码
#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 7;
ll a[MAXN],pre[MAXN],dp[105][MAXN];
/*
5 2
1 -2 5 -3 4
*/
int main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i <= n;++i)	scanf("%lld",&a[i]);
	
	memset(dp,0xcf,sizeof dp);
	dp[0][0] = 0;
	int o = 1;
	
	for(int j = 1;j <= m;++j)
	{
		for(int i = 1;i <= n;++i)
		dp[j][i] = max(pre[i - 1],dp[j][i - 1]) + a[i];
		
		pre[0] = -1e18;
		for(int i = 1;i <= n;++i)
		pre[i] = max(pre[i - 1],dp[j][i]);
	}
	
	ll ans = -1e18;
	for(int i = 1;i <= n;++i)	ans = max(ans,dp[m][i]);
	printf("%lld\n",ans);
	return 0;
}

2022-02-23

涂色

我们很容易得到转移方程:
f[i]表示涂前i个需要的最小代价
转移:
f[i] = min(f[j] + (j + 1 ~ i中的不同数个数的平方))
我们观察一下最后的答案区间,肯定是不会超过n的,因为我只需要全都作为一个去拿,就可以得到这个答案
回到上面那个式子,说明我们只需要枚举cnt * cnt <= n即可
这样时间复杂度就是n*(sqrt(n) + C)
这个常数C可能会很大,因此还需要进一步优化
我们发现,当前枚举到第j为,若继续向前枚举的话,实际上是可以直接跳过j ~ i中出现过的数字的(这个数字就可以不用枚举)
例如1 2 3 4 5 1 2 3
当前i是8,j是4,那么对于最开始1 2 3 我们就可以直接跳过了,因为对答案不会产生贡献,只需要找最前面会改变cnt即可~、
这里用一个链表维护即可。
AC代码:

点击查看代码
#include <iostream>
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e4 + 7;
int a[MAXN],pre[MAXN],nex[MAXN];
int f[MAXN];
int cnt = 0;
unordered_map<int,int> mp;
/*
8
2 2 2 3 4 2 2 3 

8
2 3 2 2 3 3 2 3
*/
void del(int x)
{
	pre[nex[x]] = pre[x];
	nex[pre[x]] = nex[x];
}
int main()
{
	int n;
	scanf("%d",&n);
	for(int i = 1;i <= n;++i)
	scanf("%d",&a[i]);
	
	for(int i = 1;i <= n;++i)
	{
		f[i] = f[i - 1] + 1;
		int cnt = 1;
		
		pre[i] = i - 1;
		nex[i] = i + 1;
		if(mp.count(a[i]))
		{
			int id = mp[a[i]];
			del(id);
		}
        int j;
		for(j = pre[i];j;j = pre[j])
		{
            if(cnt * cnt > n)    break;
			f[i] = min(f[i],f[j] + cnt * cnt);
			cnt += 1;
		}
        if(j == 0)
		f[i] = min(f[i],f[0] + cnt * cnt);
		
		mp[a[i]] = i;
	}
	printf("%d\n",f[n]);
	return 0;
}

Cut sequence

方程:f[i]表示前i个序列,满足区间和<=m的最小的切片区间最大值和
和上一题差不太多,不过是变了一下维护的东西。
这里是拿区间最大值说事,我们只需要枚举从第j个开始的满足j ~ i的和 <= m即可
然后由于只有个别较大的数才会改变转移中的决策,而且对于一个区间内的最大值,我们最好是尽量地向前取更长的区间,例如:
w 8 4 5 6
id 1 2 3 4
当前区间的最大值如果是6,我们4 5 都不需要去枚举,只需要从8转移即可
此时id = 1
即dp[i] = min(dp[i],dp[id] + 6);
然后id = 4再转移即可
用一个双端队列来维护即可
AC代码

点击查看代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int MAXN = 1e5 + 7;
typedef long long ll;
int a[MAXN];
ll s[MAXN],dp[MAXN];
int stk[MAXN],top = 0;
/*
1 2
2
*/ 
int main()
{
	int n;ll m;
	scanf("%d %lld",&n,&m);
	for(int i = 1;i <= n;++i)
	{
		scanf("%d",&a[i]);
		s[i] = s[i - 1] + a[i];
	}
	
	deque<int> q;
	memset(dp,0x3f,sizeof dp);
	
	dp[0] = 0;
	for(int i = 1;i <= n;++i)
	{
		if(a[i] > m)	{
			puts("-1");
			return 0;
		}
		while(q.size() && s[i] - s[q.front() - 1] > m)	q.pop_front();
		while(q.size() && a[q.back()] <= a[i])	q.pop_back();
		
		q.push_back(i);
		dp[i] = dp[i - 1] + a[i];
		{
			deque<int> now = q;
			int id = lower_bound(s + 1,s + i + 1,s[i] - m) - s;
			
            for(int j = -5;j <= 5;++j)
            {
                if(id + j >= 1 && id + j <= i)
                {
                    if(s[i] - s[id + j - 1] <= m){
	                    id = id + j;
	                    break;
                	}
                }
            }
            
			int ls = id;
			dp[i] = min(dp[i],dp[id - 1] + a[q.front()]);
			while(now.size())
			{
				ls = now.front();
				now.pop_front();
				
				int Max;
				if(now.size())	Max = a[now.front()];
				dp[i] = min(dp[i],dp[ls] + Max);
			}
		}
	}
	printf("%lld\n",dp[n]);
}

2022-02-25

最近在做CSP的题,好难啊!

登机牌条码

先找到所有的码元,具体按照题意模拟就行...这个是其中较为简单的一部分
还有一部分是求校验码,学过CRC应该就能明白这个式子。其实要求的r(x)就是d(x)除以g(x)的余数的负数...
因此我们先多项式乘法,将g(x)展开,
然后再多项式除法,将d(x)所有位数上的数字都归零,最后剩下的k个就是-r(x)了
AC代码:

点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 929;
string s;
int w,k;
vector<ll> a,ans;
const int MAXN = 5e5 + 7;
ll c1[MAXN],c2[MAXN];
ll ksm(ll a,ll b)
{
	ll t = 1;
	while(b)
	{
		if(b & 1)	t = t * a % MOD;
		a = a * a % MOD;
		b >>= 1;
	}
	return t % MOD;
}
int main()
{
	scanf("%d %d",&w,&k);
	cin >> s;
	int now = 0;//o -> 大写  1 ->  小写   2 -> 数字 
	
	for(int i = 0;i < s.length();++i)
	{
		if(s[i] >= '0' && s[i] <= '9')
		{
			if(now != 2)
			a.push_back(28);
			now = 2;
			a.push_back(s[i] - '0');
		}
		else if(s[i] >= 'a' && s[i] <= 'z')
		{
			if(now != 1)
			a.push_back(27);
			now = 1;
			a.push_back(s[i] - 'a');
		}
		else if(s[i] >= 'A' && s[i] <= 'Z')
		{
			if(now == 1)
			{
				a.push_back(28);
				a.push_back(28);
			}
			else if(now == 2)
			a.push_back(28);
			now = 0;
			a.push_back(s[i] - 'A');
		}
	}
	if(a.size() & 1)	a.push_back(29);
//	for(int i = 0;i < a.size();++i)
//	printf("%d%c",a[i], " \n"[i == a.size() - 1]); 
	
	ans.push_back(0);
	for(int i = 0;i < a.size();i += 2)
	{
		int now = 30 * a[i] + a[i + 1];
		ans.push_back(now);
	}
	
	int cnt;
	if(k == -1)	cnt = 0;
	else cnt = (1 << (k + 1));
	
	while((ans.size() + cnt) % w != 0)
	ans.push_back(900);
	
	ans[0] = ans.size();
	
	//进入检验码环节
	if(cnt == 0)
	{
		for(int i = 0;i < ans.size();++i)
		printf("%lld\n",ans[i]);
		return 0;
	}
	//1、展开多项式 2、多项式除法
	ll pw = ((-3 % MOD) + MOD) % MOD;
	c1[0] = pw,c1[1] = 1;
	for(int i = 2;i <= cnt;++i)
	{
		pw = pw * 3 % MOD;
		for(int j = 0;j <= i - 1;++j)
		{
			c2[j + 0] += ((pw * c1[j] % MOD) + MOD) % MOD;
			c2[j + 0] %= MOD;
			c2[j + 1] += c1[j];
			c2[j + 1] %= MOD;
		}
		
		for(int j = 0;j <= i;++j)	c1[j] = c2[j],c2[j] = 0;
	}
//	for(int i = 0;i <= cnt;++i)
//	printf("%d ",c1[i]);
	//开始多项式除法
	int N = ans.size();
	for(int i = 0;i < ans.size();++i)
	c2[i] = ans[i];
	
	reverse(c1,c1 + cnt + 1);
	
	for(int i = 0;i < N;++i)
	{
		ll div = c2[i] % MOD;
		for(int j = i;j <= cnt + i;++j)
		{
			c2[j] -= div * c1[j - i] % MOD;
			c2[j] = ((c2[j] % MOD) + MOD) % MOD;
		}
	}
	for(int i = N;i <= N + cnt - 1;++i)
	ans.push_back(((-c2[i] % MOD) + MOD) % MOD);
	
	for(int i = 0;i < ans.size();++i)
	printf("%lld\n",ans[i]);
	return 0;
}

磁盘文件操作

离线离散化+线段树维护;
细节较多...
还未写出能AC代码,明天补了,睡大觉55555太菜了

posted @ 2022-02-21 23:17  K0njac  阅读(30)  评论(0编辑  收藏  举报