2025/7/11总结
2025/7/11\(\mathbf{} \begin{Bmatrix} \frac{{\Large Practice} }{{\color{Yellow}\Large Record} }\mathbf{} {No.4} \end{Bmatrix}\times{}\) NeeDna
题意:森林的地图有 \(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;
}
题意:给你一棵树,让你把它剖成 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;
}
题意:一个点初始值为 \(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;
}
一道放了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;
}
个人感觉是黄或者绿色。
题意:
设 \(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\)类:
它们分别是:
- \(a[i] = a[i-1]\) 那么插入最大值和最小值。
- \(a[i] > a[i-1]\) 那么插入两个最大值。
- \(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;
}

浙公网安备 33010602011771号