2020.12.01~2020.12.04 题记

1、P3698 小Q的棋盘

题目大意

一棵大小为\(n\)的树,节点为 \(0\) ~ \(n-1\),从根节点 \(0\) 开始走 \(v\) 步,求最多可已经过多少个不同的节点(可以往回走)?

思路

一开始看,认为是树形DP,设 \(f_{i,j}\) 为花费 \(i\) 步后到达 \(j\) 节点时的最大经过节点个数。于是就想了个很假的转移方程:

\[f_{i,j}=\max(f_{i,j},f_{i-1,k}) \]

其中 \(k\)\(j\) 的父亲或儿子。

打着打着发现无法记录哪个点被经过,于是就假了。

考虑新的转移方程。如果需要从儿子返回到它的父亲节点,那就会浪费掉一次走的贡献——父亲节点已经走过了,所以我们应该尽可能少地重复走一条路径。容易发现,找一条长度大于等于 \(v\) 的简单路径一直往下走,则我们所走的每一步都不会被浪费,经过的节点数必然是最多的(见样例1)。

但是可能这棵树上最长的一条简单路径都小于 \(v\)。怎么办呢?多余的步数我们必须往外面走。假设我们在走一条链,走完之后就要往外面走。如果我们一直在这条链上走到底,那么我们就必须先回去再去探索其他路径,这很麻烦。所以一种更优的走法就是走到那个分叉口后直接往别的地方走,走完再回来。这样我们就会多走一段路径,也就是要走两步才可以为答案贡献1(个节点)。

可以证明,无论我们走的是哪条链,分叉出去的路径是怎么样的,都要走2步才可以为答案贡献1(个节点)。所以答案就是链的节点数+剩余路径/2。

可是这就有了一个问题:我们该选哪条链?而且分叉出去可以不回来啊。先不考虑分叉出去不回来的情况,最佳答案便是最长链(怎么变成贪心了?);其次如果分叉出去不回来,那么就相当于以分叉出去的那一条路径代替了原来不分叉后面的路径,那一条链就变成了另外一条链,计算方式仍然相同。所以最佳方式还是选一条最长链,答案还是最长链的节点数+剩余路径/2。

至此为止,我们将DP转成了贪心的思路,并证明了它是正确的。打一发交上去——50分?哦,对了,注意总共经过的节点是不可能超过整棵树的结点数:\(n\) 的。所以让答案与 \(n\) 取个 \(\min\) 就行了。

代码

#include<bits/stdc++.h>
using namespace std;
using namespace my_std;
ll n,v,f[101][101],head[101],cnt=0,gsiz[101],gson[101],sum=0;
struct node{
	ll nxt,to;
}e[101];
void add(ll u,ll v){
	e[++cnt].nxt=head[u];
	e[cnt].to=v;
	head[u]=cnt;
}
void gg(ll fa,ll u){
	go(u){
		ll v=e[i].to;
		if(v==fa) continue;
		gg(u,v);
		if(gsiz[u]<(gsiz[v]+1)){
			gsiz[u]=gsiz[v]+1;
			gson[u]=v;
		}
	}
}
int main(){
	n=read();
	v=read();
	fr(i,2,n){
		ll u=read(),v=read();
		add(u,v);
		add(v,u);
	}
	gg(-1,0);
	fr(i,0,n-1) if(!gson[i]) gson[i]=-1;
	for(int i=0;i!=-1;i=gson[i]) sum++;
	sum--;
	if(sum>=v) write(v+1);
	else write(min(n,sum+1+(v-sum)/2));
}

2、P4989 二进制之谜

题目大意

给一个01串,让你选择其中的一个 \(0\)\(1\) 组成一对(其中 \(0\)\(1\) 前面),定义它们的 “启发系数” 为它们所在位置的差的绝对值。

现在让你选择尽可能多这样的数对,其中每对数对所在位置中间的部分不能相交,但可以包含,且每个位置的数只能被配对一次。求总 “启发系数” 的最大值。

思路

一开始看到数据范围,肯定会想到那种最经典的区间DP:

\[f_{i,j}=\max(f_{i,j},f_{i,k}+f_{k+1,j}(i \leq k < j ),j-i+f_{i+1,j-1}(!a_{i}\&\&a_{j})) \]

交上去:20pts。为什么呢?

经过仔细读题,才发现漏了一个最重要的一个条件:尽可能多这样的数对

既然是最多,那么肯定要让每个 \(0\) 后面都跟着1个 \(1\)。抛开最大值中间的部分不能相交,但可以包含,如果要尽可能多,那必然是每找到1个 \(0\) 之后就与它后面紧跟着的那个 \(1\) 匹配。举个例子理解一下,比如这个样例:

4
0101

如果我们按照DP思想,先匹配了两端的两个数,那只能成功匹配1组。但如果按照上面的思想,却可以匹配2组,因为我们要尽可能的用近一点的 \(1\) ,否则会影响后面 \(0\) 的匹配,即 “我有可以匹配的1却偏偏占了其它0所匹配的1,导致其它的0不能匹配” 。

回过头来看这个条件:中间的部分不能相交,但可以包含。举个例子,我有两组数对,第一组的0位置是 \(l_1\),1位置是 \(r_1\),第二组的0位置是 \(l_2\),1位置是 \(r_2\),其中 \(l_1 < l_2 < r_1 < r_2\)。按照题目上来说,这两个数对是相交且不是包含关系的。如果不考虑条件,那么它们对答案的贡献就是 \(r_1 - l_1 + r_2 - l_2\)。现在,我们将这两个数对稍稍变化一下,把 \(r_1\)\(r_2\) 对换一下——这时候这两个新数对就成了包含数对,符合了原来的要求。再看这两个新数对对答案的贡献:\(r_2 - l_1 + r_1 - l_2\),发现跟上面一模一样,只是顺序的问题。也就是说,这个条件有跟没有是一样的,两个相交的数对我们照样算进答案里(因为可以把它们如上述一样操作,变成合法的两个数对,且贡献不变)。

所以枚举01串,看到 \(0\) 就把它的位置上塞到一个队列里,看到 \(1\) 就从队列里最前面的那个 \(0\)\(pop\) 掉(因为1匹配时0要尽可能前,相当于0匹配1时1要尽可能不那么后),进行匹配,算出答案。

再一交:30pts。又是为什么呢?

再给一个样例:

3
011

按照上述操作,算出来答案为1,但是很容易发现答案为2。这就是因为我们没有考虑到这种情况:“我没有可匹配的0,但是我可以从别人那里夺走0,总而对答案的贡献比之前要大”,当然这是在那个1前面没有无匹配的0的情况下。所以我们考虑返回操作:让每个已匹配的数对也塞入队列里,如果枚举到的1前面满足这种情况,就从已匹配的数对中取一对,减去之前的贡献再加上之前的贡献。

很明显,答案贡献要最大,那就必须从已匹配数对中取一个数对,使得这个数对中1的位置与目前为止最远——即要尽可能比之前多贡献。所以维护一个结构体存这个数的原本位置和要用来比较的权值,再定义一个小根堆,按照结构体的权值排序。

怎么定义权值呢?如果插入的是0,因为1匹配时0要尽可能前,所以就按它原本的位置当权值,保证从小到大,与1匹配时答案就加上(目前的位置-它的位置)。当插入1个数对时,它的权值要比所有插入的0的权值都要大(因为没有0取了才能抢走别的数对的0),于是权值就定义为 \(n\) 加上数对中1的位置,结构体中的 “原本位置” 也就是数对中1的位置。为什么是1的位置呢?因为我们答案要减去之前的贡献再加上之前的贡献,也就是加上多出来的贡献,其实就是目前的位置减去那个数对中1的位置。这样的答案计算方式也恰好等于上种情况的计算方式,不用麻烦地特判。

(怎么又从DP变成贪心了?QAQ)

代码

#include<bits/stdc++.h>
using namespace std;
using namespace my_std;
ll n,a[30003],ans=0;
struct node{
	ll pos,w;
}b[30003];
priority_queue<node> q;
bl operator <(node x,node y){
	return x.w>y.w;
}
node make_node(ll pp,ll ww){
	node res;
	res.pos=pp;
	res.w=ww;
	return res;
}
int main(){
	n=read();
	fr(i,1,n){
		char c;
		cin>>c;
		a[i]=c-'0';
	}
	fr(i,1,n){
		if(!a[i]) q.push(make_node(i,i));
		else{
			if(q.empty()) continue;
			node u=q.top();
			q.pop();
			ans+=i-u.pos;
			q.push(make_node(i,n+i));
		}
	}
	write(ans);
}

3、P1052 过河

题目大意

你要过一个长度为 \(l\) 的桥,每次只能往前走 \(s\) ~ \(t\) 个单位。桥上有 \(m\) 个石子在不同的位置上。现在你要从 \(0\) 出发过桥,求经过的最少石子个数。

思路

看到 \(l\) 的范围,就知道不能直接死DP,于是就想到用 \(m\) 作DP的范围。当时想到了一个很假的转移方程:

\[f_i=min(f_i,f_{i-k}(s \leq k \leq t))+1 \]

但是发现走到每个石子的最值答案可能来源于没有石子的那个地方,且因为只枚举 \(m\),不能知道走到没有石子的地方的最值答案(即从 \(0\) 走到此石子所要经过的石子的最小值)。由于这题是个明显的DP,于是考虑缩小每个石子间的距离。

首先思考一个石子能否刚好走到下一个石子。假设我们只能走 \(k\)\(k+1\) 步,所以我们一定能通过走 \(k+1\) 次距离为 \(k\) 的方法来达到距离这个点 \(k(k+1)\) 的点。现在来简单地证明距离这个点 \(k(k+1)\) 以外的点都能被走到。

1.当距离 \(len\) 小于 \(2 \times (k+1)\) 时,我们只需把前面所走的 \(k+1\) 次距离为 \(k\) 的路程中,选取 \(len-(k+1)\) 次,将行走的距离改为 \(k+1\) 即可。

2.当距离 \(len\) 小于 \(2 \times (k+1)\) 时,我们可以走尽可能多的距离为 \(k(k+1)\) 的那种走法,剩余的 \(len \mod k(k+1)\) 的路程,就可转换为第1种。

至此,我们证明了当石子之间距离大于等于 \(s(s+1)\)\(s<t\) 时,都可以从前一个石子走到后一个石子。根据 \(s<=t<=10\),我们只需将两石子之间距离 \(len\) 缩短至 \(\min(len,90)\) 即可(因为这种情况下 \(s_{max}=9\)\(t_{max}=10\))。这样做是不会影响答案的。因为缩短前石子最值的来源只能是它前面的与它距离为 \(s\) ~ \(t\) 的地方,而因为 \(t < 90\),它这些可能来源的地方是没有被缩去的,只是压缩了中间不能为答案作贡献的路径罢了。

那么 \(s=t\) 的情况怎么做呢?那就更简单了。每一步只能走 \(s\) 个单位的距离,所以只用判断石子所在的位置模上 \(s\) 等不等于 \(0\),等于就将答案加一。

代码

#include<bits/stdc++.h>
using namespace std;
using namespace my_std;
ll l,s,t,m,a[101],b[101],f[1100000],ans=0;
bl ck[1100000];
int main(){
	l=read();
	s=read();
	t=read();
	m=read();
	if(s==t){
		fr(i,1,m){
			a[i]=read();
			if(a[i]%s==0) ans++;
		}
		write(ans);
		return 0;
	}
	fr(i,1,m) a[i]=read();
	sort(a+1,a+m+1);
	fr(i,1,m){
		b[i]=b[i-1]+min(a[i]-a[i-1],90*1ll);
		ck[b[i]]=1;
	}
	fr(i,1,b[m]+10){
		f[i]=0x7f7f7f7f;
		fr(j,s,t){
			if((i-j)<0) continue;
			f[i]=min(f[i],f[i-j]);
		}
		if(ck[i]) f[i]++;
	}
	ans=0x7f7f7f7f;
	fr(i,b[m],b[m]+10) ans=min(ans,f[i]);
	write(ans); 
}

4、P1245 电话号码

题目大意

仿照电话机上的按键,26个字母都可被表示为 \(1\) ~ \(9\) 之间的数字(不懂看原题)。现在给你一串数字,几个单词,让你用这些单词来翻译数字串(每个单词间有空格但空格不占对应的位置,翻译出来的单词串中每个字母要与数字串对应,对应关系如前面所提)。

思路

很明显的背包。这里提供另外一种做法,想用背包做的可以自己尝试。

见到这数据范围:为什么不用搜索呢?直接枚举没用过的单词(这里没太看懂可不可以重复用,但是不重复用可以过),然后判断数字串中目前位置(这里指没匹配到的第一个数字)往后的数字能不能被此单词匹配,如果可以匹配就加到答案中,继续搜索下去。这样我们搜索就只用传递目前匹配到的数字串位置就行了。

将字符串化成数字,可以直接用数字存它们的对应关系。为了简化时间,在目前位置大于字符串长度时,直接输出答案并 \(exit(0)\) (意思是结束整个程序)。如果没有答案,就直接在主程序搜索后面输出 “No Solutions!”,因为如果有答案那么就会直接退出程序,不会运行到主程序搜索后的代码。

注意:行末不能有多余的空格和换行。

代码

#include<bits/stdc++.h>
using namespace std;
using namespace my_std;
ll n,m,a[101],mp[27]={0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,6,6,6,7,7,7,8,8,8,9,9,9};
ll ans[101],cnt=0;
string s[101],ss;
bl ck[101];
void dfs(ll now){
	if(now>m){
		fr(i,1,cnt-1){
			cout<<s[ans[i]];
			cout<<" ";
		}
		cout<<s[ans[cnt]];
		exit(0);
	}
	fr(i,1,n){
		if((now+s[i].size())>(m+1)||ck[i]) continue;
		bl pd=0;
		fr(j,now,now+s[i].size()-1){
			if(a[j]!=mp[s[i][j-now]-'a'+1]){
				ll k=mp[s[i][j-now]-'a'+1];
				pd=1;
			}
		}
		if(!pd){
			ck[i]=1;
			now+=s[i].size();
			ans[++cnt]=i;
			dfs(now);
			cnt--;
			ck[i]=0;
			now-=s[i].size();
		}
	}
}
int main(){
	n=read();
	cin>>ss;
	m=ss.size();
	fr(i,1,m) a[i]=ss[i-1]-'0';
	fr(i,1,n) cin>>s[i];
	dfs(1);
	pf("No Solutions!");
}
posted @ 2020-12-01 14:27  AFewSuns  阅读(144)  评论(0编辑  收藏  举报