2025/7/11总结

2025/7/11\(\mathbf{} \begin{Bmatrix} \frac{{\Large Practice} }{{\color{Yellow}\Large Record} }\mathbf{} {No.4} \end{Bmatrix}\times{}\) NeeDna

[CSP-S 2023] 种树

题意:森林的地图有 \(n\) 片地块,其中 \(1\) 号地块连接森林的入口。

共有 \(n−1\) 条道路连接这些地块,使得每片地块都能通过道路互相到达。

你每天可以选择一个未种树且与某个已种树的地块直接邻接(即通过单条道路相连)的地块,种一棵高度为 \(0\) 米的树。

在第 \(x\) 天,\(i\) 号地块上的树会长高 \(max(b_{i}+x×c _{i} ,1)\)米。注意这里的 \(x\) 是从整个任务的第一天,而非种下这棵树的第一天开始计算。

你想知道:最少需要多少天能够完成你的任务?

做法:这道题二分很有意思,因为越早种树答案越大,而且题目明显是二分,那么二分终点,对于每个点算出最晚种下时间。

对于么个点的高度计算因为是等差数列求和所以优化到 \(O(1)\) 求时间。

然后把时间在树上dp一下,有\(父亲最晚时间=max(father时间,son时间-1)\)

最后判断答案合理与否很有意思,最后只要对于每个 \(i(1\le i\le n)\),最晚种树时间不超过 \(i\) 的点的个数 \(\le i\) 就可以了,做一遍前缀和即可。

注意,取最小值以后最晚种树时间可能 \(\le0\),要特判。求一段时间内的生长高度时需要用 __int128,会爆 long long

时间复杂度 \(O(n\log V\log n)\),常数很小。

code:

#include<bits/stdc++.h>
#define ll long long
#define pb push_back
using namespace std;
const int N=1e5+10;
vector<int> g[N];
ll a[N],b[N],c[N],n,p[N],ct[N];
inline __int128 get(ll i,ll n,__int128 b,__int128 a){
    if(a<0){
        ll d=min((b-a-1)/(-a),(__int128)n+1);
        if(d<=i)return n-i+1;
        return n-d+1+(d-i)*b+(d-1+i)*(d-i)/2*a;
    }
    return (n+i)*(n-i+1)/2*a+b*(n-i+1);
}
void dfs(int u,int fa){
	for(int v:g[u]){
		if(v==fa) continue;
		dfs(v,u);
		p[u]=min(p[u],p[v]-1);
	}
}
bool check(int mx){
	for(int i=1;i<=n;i++){
		if(a[i]>get(1,mx,b[i],c[i])) return 0;
		int l=1,r=n;
		while(l<r){
			int mid=(l+r+1)>>1;
		    if(a[i]<=get(mid,mx,b[i],c[i])) l=mid;
		    else r=mid-1;
		}
		if(i==1) l=1;
		p[i]=l;ct[i]=0;
	}
	dfs(1,n);
	for(int i=1;i<=n;i++){
		if(p[i]<=0) return 0;
		ct[p[i]]++;
	}
	for(int i=1;i<=n;i++){
		ct[i]+=ct[i-1];
		if(ct[i]>i) return 0;
	}
	return 1;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i];
	for(int i=1,u,v;i<n;i++){cin>>u>>v;g[u].pb(v),g[v].pb(u);}
	int l=n,r=1e9;
	while(l<r){
		int mid=l+r>>1;
		if(check(mid)) r=mid;
		else l=mid+1;
	}
	cout<<l;
	return 0;
} 

[NOIP 2018 提高组] 赛道修建

题意:给你一棵树,让你把它剖成 m 条链,问其中最短的链最长是多少。

做法:首先看出来是二分,那么就二分最短长度,但是很显然不是很好求,我们来做一个dp。

对于一个子树,应该会有很多链满足要求,还有一些点死活满足不了,为了方便转移,我们可以找出来经过father的链而且满足无法达成要求的点的最大值,用它来向上走是最有可能再凑出来满足路线的。

标准一点:我们先开一个数组 \(b\)\(b_u\) 就表示以 \(u\) 为根的子树中,所有以 \(u\) 为起点的链中长度没有达到 \(mid\) 的链或者不能与另一条没有达到 \(mid\) 的链拼在一起来超过 \(mid\) 的链中最长的那条链的长度。

Part 1:遍历子树

遍历 \(u\) 时,先枚举它的所有儿子,设儿子的编号为 \(v\),到 \(u\) 的边的长度为 \(w\),我们先 DFS(v),然后如果 \(b_v+w\ge mid\),就将 \(tot\) 加 1,否则就将其存入数组 \(a\) 中。

Part 2:合并链

我们将 \(a\) 数组从小到大排序,然后从小到大枚举,对于每个 \(i\),如果能找到一个 \(j\),使 \(a_i+a_j\ge mid\),且 \(j\) 最小,那么就将 \(tot\) 加 1,然后把 \(i\)\(j\) 标记一下,这一部分可以用二分做。然后如果 \(j\) 已经被标记过了,就把 \(j\) 往后跳,直到 \(j\) 没有被标记,如果跳出去了就不管它。注意不能想当然的用双指针,因为可能开始已经把一些点跳过了,但它们并没有被标记,导致后面可能有些链本来能匹配的,却没有匹配到。

Part 3:上传 \(b\) 数组

我们从剩余的没有标记的链中取个最大值,传进 \(b_u\) 中,然后这题就做完了,时间复杂度 \(O(n\log^2n)\)

code:

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=5e4+10;
int n,m,mid,tot,a[N],b[N],cnt,tag[N];
struct edge{int v,w;};
vector<edge> g[N];
void dfs(int u,int fa){
	for(auto[v,w]:g[u]){
		if(v!=fa) dfs(v,u);
	}cnt=0;
	for(auto[v,w]:g[u]){
		if(v==fa) continue;
		if(b[v]+w>=mid)++tot;
		else a[++cnt]=b[v]+w;
	}sort(a+1,a+cnt+1);
	fill(tag+1,tag+cnt+1,0);
	for(int i=1;i<cnt;i++){
		if(tag[i]) continue;
		int j=lower_bound(a+i+1,a+cnt+1,mid-a[i])-a;
		if(j==cnt+1) continue;
		while(tag[j]&&j<cnt) j++;
		if(!tag[j]) tag[i]=tag[j]=1,++tot;
	}b[u]=0;
	for(int i=1;i<=cnt;i++) if(!tag[i]) b[u]=a[i];	
}
bool check(){
	tot=0,dfs(1,0);
    return tot>=m;
}
int main(){
	cin>>n>>m;
	for(int i=1,a,b,l;i<n;i++){
		cin>>a>>b>>l;
		g[a].pb(edge{b,l});
		g[b].pb(edge{a,l});
	}
	int l=1,r=1e18,ans=0;
	while(l<=r){
		mid=l+r>>1;
		if(check()) ans=mid,l=mid+1;
		else r=mid-1; 
	}
	cout<<ans;
	return 0;
}

[LNOI2022] 吃

题意:一个点初始值为 \(1\),有n个操作,顺序随意。每个操作有 \(2\) 种,对于操作 \(i\) 分别是 $ \times a_{i}$ 和 \(+b_{i}\)。求最终值的max。(答案mod 1e9+7)

答案很明显是先选部分加法,然后再乘。(举个例子易证)。

还有一个显然的结论:当 \(a_{i}=1\) 时一定选择加法,应为乘 \(1\) 卵用没有。

接下来思考 \(a_{i}>1\) 时怎么搞,这个很有意思。

先说结论:\(a_{i}>1\) 的集合中,最多选 \(1\)个加法

证明:

并且对于 \(a_i \neq 1\) 的二元组,我们最多只会选择一个数进行加法。反证,假设进行两次加法 \(b_i, b_j\),不失一般性另 \(b_i \ge b_j\),由于 \(a_j \ge 2\),所以 \(2b_i \ge b_i + b_j\),第二次选择乘法会更优。

做法:这样我们只用枚举哪个数加即可,另 \(K = \prod a_i\),如果不加答案为 \(KS\),如果加 \(b_i\) 那么答案为 \(\dfrac{K(S+b_i)}{a_i}\),所以我们只用选出 \(\dfrac{S+ b_i}{a_i}\) 最大的二元组即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+100,mod=1e9+7;
int n,a[N],b[N],x=1,ans,y=1;
long double q;
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++){cin>>b[i];if(a[i]==1) x+=b[i];}
	q=x;//<-----初始值要这个,因为可以不选任何ai>1的
	for(int i=1;i<=n;i++){
		if(a[i]!=1&&(x+b[i])*1./(1.0*a[i])>q){
			q=(x+b[i])*1./(1.0*a[i]);
			ans=i;
		}
	}x+=b[ans];
	for(int i=1;i<=n;i++) if(i!=ans) x=(x*a[i])%mod;
	cout<<x%mod<<'\n';
	return 0;
}

[USACO2.3] 最长前缀 Longest Prefix

题意:给了一个元素集和一个串,问用元素集中元素可以表示串的最长前缀的长度。 \(O(n \times card(P)\)

一个串长 \(i\) 满足的充要条件是:存在一个比i小的j,满足串长 \(j\) 可以满足且从第 \(j+1\) 到第 \(i\)\(p\) 中的一个元素。

然后用substr暴力判断是否相同即可

code:(from hhjtutuhe)

#include<bits/stdc++.h>
using namespace std;
int n,ans;
string P[201],S=" ";
bool f[200001]= {1};
bool Check(int p){
  for(int i=0;i<n;i++){
  	int t=P[i].size();
  	if(p>=t && f[p-t] && P[i]==S.substr(p-t+1,t)){
      ans=p;
      return true;
	}
  }
  return false;
}
int main(){
  for(string s;cin>>s,s!=".";P[n++]=s);
  for(string s;cin>>s;S+=s);
  for(int i=1;i<=S.size();i++)
    f[i]=Check(i);
  printf("%d\n",ans);
  return 0;
}

[USACO09NOV] Lights G

一道放了2个月的题,终于改出来了,收获了一个教训:

(1<<35) 返回是负数,不论是否开了longlong 要改成 (1ll<<35)!!! 我改了1h。

题意

给出一张 \(n\) 个点 \(m\) 条边的无向图,每个点的初始状态都为 \(0\)

你可以操作任意一个点,操作结束后该点以及所有与该点相邻的点的状态都会改变,由 \(0\) 变成 \(1\) 或由 \(1\) 变成 \(0\)

你需要求出最少的操作次数,使得在所有操作完成之后所有 \(n\) 个点的状态都是 \(1\)

这道题恰好可以折半搜索,把前 \(n/2\) 个点搜索求答案放map里,后面一样,最后看一下是否存在两部分合并为全亮 \(O(2\times 2^{\frac{n}{2}})\)

没啥好说的,开longlong用数字存状态,类似压缩状态。

code:

#include<bits/stdc++.h>
#include<unordered_map>
#define int long long
#define push_back pb
using namespace std;
const int N=1e6+10;
unordered_map<int,int> mp;
int n,m,a[50],ans=50;
void dfs1(int g,int s,int cnt){
	if(g>(n>>1)){
		if(!mp[s]) mp[s]=cnt;
	    else mp[s]=min(mp[s],cnt);
	    return;
	}
	dfs1(g+1,s^a[g],cnt+1);
	dfs1(g+1,s,cnt);	
}
void dfs2(int g,int s,int cnt){
    if(g>n){
    	if(mp[(((1ll<<(n+1))-2)^s)]||s==((1ll<<(n+1))-2)){		
    	    ans=min(ans,cnt+mp[(((1ll<<(n+1))-2)^s)]);	
		}
	    return;
	}
	dfs2(g+1,s^a[g],cnt+1);
	dfs2(g+1,s,cnt);		
}
signed main(){
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;a[u]^=(1ll<<v);
		a[v]^=(1ll<<u);
    }for(int i=1;i<=n;i++) a[i]^=(1ll<<i);
    dfs1(1,0,0);dfs2((n>>1)+1,0,0);
    cout<<ans;
	return 0;
} 

[BalkanOI 2011] medians

个人感觉是黄或者绿色。

题意:

\(A\)\(1,2,3,\ldots,2\times N-1\) 的任意一种排列。

我们定义 \(A\) 的前缀中位数为一个长度为 \(N\) 的数列 \(B\)\(B_i\)\(A_1,A_2,\ldots,A_{2\times i-1}\) 的中位数。

我们将会给出 \(B\) 数列,请构造一个数列 \(A\),使得其前缀中位数为 \(B\)

首先第一个数可以直接确定,对于接下来的数,我们对于每一个中位数,都会在原序列中加入 \(2\) 个数。

怎么加呢,假如这个数在答案序列中没有出现过,我们就先把他加上,然后调整ans,大于前一个中位数就加上一个大的数均衡一下,反之加一个小的数。

如果答案序列中出现过这个数,那么就分成 \(3\)类:

它们分别是:

  1. \(a[i] = a[i-1]\) 那么插入最大值和最小值。
  2. \(a[i] > a[i-1]\) 那么插入两个最大值。
  3. \(a[i] < a[i-1]\) 那么插入两个最小值。

关于怎么选大数和小数,其实就是选不重复的最大最小,这样最优,而且一定可以调整。

思路明确,贪心即可。代码看着长实际很简单就能理解而且很好写。

code:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,a[N],l=1,r,vis[N<<1],ans[N<<1],cnt;
int main(){
	cin>>n;r=n*2-1;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		if(i==1){
			vis[a[i]]=1;
			ans[++cnt]=a[i];
		}
		else{
			if(!vis[a[i]]){
				vis[a[i]]=1;
				ans[++cnt]=a[i];
				if(a[i]<a[i-1]){
					while(vis[l]) l++;
					ans[++cnt]=l;
					vis[l]=1;
				}
				else{
					while(vis[r]) r--;
					ans[++cnt]=r;
					vis[r]=1;
				}
			}
			else{
				if(a[i]<a[i-1]){
					while(vis[l]) l++;
					ans[++cnt]=l;
					vis[l]=1;
					while(vis[l]) l++;
					ans[++cnt]=l;
					vis[l]=1;
				}
				else if(a[i]>a[i-1]){
					while(vis[r]) r--;
					ans[++cnt]=r;
					vis[r]=1;
					while(vis[r]) r--;
					ans[++cnt]=r;
					vis[r]=1;
				}
				else{
					while(vis[l]) l++;
					ans[++cnt]=l;
					vis[l]=1;
					while(vis[r]) r--;
					ans[++cnt]=r;
					vis[r]=1;
				}
			}
		}
	}
	for(int i=1;i<=n*2-1;i++) cout<<ans[i]<<" ";
	return 0;
}
posted @ 2025-07-11 21:00  NeeDna  阅读(18)  评论(0)    收藏  举报