问题
给定一些任务,任务之间存在着一些约束关系,分为两类,冲突约束和先后约束,存在冲突约束的两个任务不能再同一天执行,先后约束的两个任务x和y,必须执行完x,才能执行y,每个任务都需要一天的时间才能完成
所以,可以构建出一棵树,一个由任务作为节点,任务之间的约束条件作为边连接成的树,冲突约束可以做为无向边,先后约束作为有向边,求至少需要多少天能够完成所有工作
定理: 删去无向边后存在的最长链长度是k,那么所需要的天数为k或k+1
分析
只要求出删除无向边的最长链长度maxlen,然后再判断是无向边定向后maxlen是否是最长链长就可以了,如果不是,答案就是maxlen+1
首先初始化,将输入数据转化为树的形式,其中每一条有向边存储两次(正向和反向)方便DFS处理和之后的DP(因为无法确定那一端是父节点)
然后使用DFS从每个点开始遍历, 找出去删除了无向边的最长链的长度maxlen(这里计算的是边长)
接下来,从root开始进行对树上的节点进行DFS,在这过程中,求出每一个点的f和g值,i是一个节点
在满足i为根节点的子树上,所有边全部定向后,最长链长度不大于maxlen时,f[i]是u->i的最长链长度,g[i]是i->u的最长链长度,u是以i为根节点的子树上的一个节点,不满足<=maxlen,就令f[i]=g[i]=Inf
DP函数的流程:按照DFS顺序求解每一个点的f和g值,对于当前点u和它的父亲fa
如果u没有子节点,那么f[u]=g[u]=0,return true;
如果u有子节点,并且和子节点之间全都是有向边,类似于w->u这种,就求出所有的w之中f的最大值,加一作为f[u],公式是
,反之,对于u->w这种,就求出所有的w之中f的最大值,加一作为g[u],公式是
,最后,评判条件是否满足
,不满足
如果u有子节点,且与子节点之间是无向边,那么先按照上述步骤处理完有向边子节点,求出u的有向边子节点中的最大f值和最大g值 ,加一得到fu和gu,再单独处理无向边子节点,对于无向边进行定向,每条边定向成
还是
呢,首先我们要求接的是定向后的满足<=maxlen条件的f[u]最小值和g[u]最小值,重定向的情况如果暴力求解时间太长,需要优化,
先看求f[u]最小值,把无向边子节点存储在集合sons中,按照f的从小到大排序,对于其中的一个点w来说,在它被定向为w->u时,那么f值比他小的那些子节点(就是从小到大排在他前面的),对于最后的f[u]值没有影响,因为他们f值都比较小,不会影响这种定向情况下的f[u],就可以把他们都定向为->u,这样还可能减小这种情况下的g[u],使得<=maxlen条件满足,所以就可以把求解最小f[u]的重定向简化为s+1种(s=sons.size())
同理对于求解最小的g[u],可以按照相同的情况处理
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn=205,Inf=1000000000;
struct Edge{
int u,v,t;
//uv是一条边,t=0代表无向,t=1代表u->v,t=2代表v->u
Edge(int u=0,int v=0,int t=-1):u(u),v(v),t(t) {}
};
struct UndirectedEdge{
int w,f,g;
UndirectedEdge(int w=0,int f=0,int g=0):w(w),f(f),g(g) {}
};
bool cmpF(UndirectedEdge &lhs,UndirectedEdge &rhs){
return lhs.f<rhs.f;
}
bool cmpG(UndirectedEdge &lhs,UndirectedEdge &rhs){
return lhs.g<rhs.g;
}
int n,root,maxlen,f[maxn],g[maxn];
//f[i]是i的子树节点到i的最长链长度,g[i]是i到它的自述中的节点的最长链长度
string str;
vector<Edge> graph[maxn];
bool readData(){
for(int i=1;i<maxn;++i) graph[i].clear();
n=0;
int u,v,t;
bool haveData=false;
while(cin>>u){
if(u==0) break;
while(cin>>str){
if(str[0]=='0') break;
if(str.back()=='d') {
v=stoi(str.substr(0,str.length()-1));
graph[u].push_back(Edge(u,v,1)); //这里存两次,一次正向,一次反向,方便求解maxlen和dp,一开始不能确定父节点
graph[v].push_back(Edge(v,u,2));
}else if(str.back()=='u'){
v=stoi(str.substr(0,str.length()-1));
graph[u].push_back(Edge(u,v,2));
graph[v].push_back(Edge(v,u,1));
}else{
v=stoi(str);
graph[u].push_back(Edge(u,v,0));//此处方便
graph[v].push_back(Edge(v,u,0));
}
++n;
}
}
root=1;
++n;
return n!=1;
}
int dfs(int u){
int ans=0;
for(int i=0;i<graph[u].size();++i){
if(graph[u][i].t==1)
ans=max(ans,dfs(graph[u][i].v)+1);
}
return ans;
}
bool DP(int u,int pa){
if(graph[u].empty()){
f[u]=g[u]=0;
return true;
}
//fu和gu是u的有向边子儿子节点中的最大f值和最大g值,fu2和gu2是u的所有子儿子节点中的最大f值和最大g值
int fu=0,gu=0,fu2=0,gu2=0;
vector<UndirectedEdge> undirectedSons;
for(int i=0;i<graph[u].size();++i){
Edge temp=graph[u][i];
if(temp.v==pa) continue;
DP(temp.v,u);
if(temp.t==1){
gu=max(gu,g[temp.v]+1);
}else if(temp.t==2){
fu=max(fu,f[temp.v]+1);
}else{
undirectedSons.push_back(UndirectedEdge(temp.v,f[temp.v],g[temp.v]));
}
}
if(undirectedSons.empty()){
if(fu+gu>maxlen) f[u]=g[u]=Inf;
else{
f[u]=fu; g[u]=gu;
}
return f[u]<Inf; //f[u]<Inf说明,fu+gu<=maxlen,说明最长链没有超过k,返回true
}
f[u]=g[u]=Inf;
int s=undirectedSons.size();
sort(undirectedSons.begin(),undirectedSons.end(),cmpF);
int maxg[maxn];
maxg[s-1]=undirectedSons[s-1].g;
for(int k=s-2;k>=0;--k){
maxg[k]=max(maxg[k+1],undirectedSons[k].g);
}
//这地方注意,k代表选择u->undirectedSons[k].w方向边的数量,k和k之前的边u->i,k之后的i->u
for(int k=0;k<=s;++k){
fu2=fu; gu2=gu; //还未划分方向
if(k>0) fu2=max(fu,undirectedSons[k-1].f+1);
if(k<s) gu2=max(gu,maxg[k]+1);
if(fu2+gu2<=maxlen) f[u]=min(f[u],fu2);
}
sort(undirectedSons.begin(),undirectedSons.end(),cmpG);
int maxf[maxn];
maxf[s-1]=undirectedSons[s-1].f;
for(int k=s-2;k>=0;--k){
maxf[k]=max(maxf[k+1],undirectedSons[k].f);
}
for(int k=0;k<=s;++k){
fu2=fu; gu2=gu; //还未划分方向
if(k>0) gu2=max(gu,undirectedSons[k-1].g+1);
if(k<s) fu2=max(fu,maxf[k]+1);
if(fu2+gu2<=maxlen) g[u]=min(g[u],gu2);
}
return f[u]<Inf;
}
int main(void){
while(readData()){
maxlen=0; //maxlen为删去无向边最长链边数
for(int i=1;i<=n;++i){
maxlen=max(maxlen,dfs(i));
}
//确定初始值
// fill(f,f+maxn,Inf);
// fill(g,g+maxn,Inf);
//本题中链长为节点数+1
if(DP(root,-1)){
printf("%d\n",maxlen+1);
}else printf("%d\n",maxlen+2);
}
return 0;
}
第一种时间复杂度是O(n*log(n))级别的,字数上还提及了第二种时间复杂度O(n^2)级别的,接下来再补充
第二种方法参考代码
表示节点i在时间j天恰好完成时,也就是说i为根节点的子树中,指向i的最长链长度f[i]恰好等于j,g[i]的最小值,所以i为根节点的子树最长链就是j+g[i]
和第一种方法相比,就是将所有的dp[i][j]求出来,显示初始化为Inf
当所有子节点都是有向的,和第一种方法一样
当子节点存在无向时,只需求出每种划分出来的的f[u]和对应的最小的g[u],然后令dp[u][f[u]]=g[u],构建dp数组,然后遍历这一行,判断是否有合法的最长链
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn=205,Inf=1000000000;
struct Edge{
int u,v,t;
//uv是一条边,t=0代表无向,t=1代表u->v,t=2代表v->u
Edge(int u=0,int v=0,int t=-1):u(u),v(v),t(t) {}
};
struct UndirectedEdge{
int w,f,g;
UndirectedEdge(int w=0,int f=0,int g=0):w(w),f(f),g(g) {}
};
bool cmpF(UndirectedEdge &lhs,UndirectedEdge &rhs){
return lhs.f<rhs.f;
}
bool cmpG(UndirectedEdge &lhs,UndirectedEdge &rhs){
return lhs.g<rhs.g;
}
int n,root,maxlen,f[maxn],g[maxn],dp[maxn][maxn];
//f[i]是i的子树节点到i的最长链长度,g[i]是i到它的自述中的节点的最长链长度
string str;
vector<Edge> graph[maxn];
bool readData(){
for(int i=1;i<maxn;++i) graph[i].clear();
n=0;
int u,v,t;
bool haveData=false;
while(cin>>u){
if(u==0) break;
while(cin>>str){
if(str[0]=='0') break;
if(str.back()=='d') {
v=stoi(str.substr(0,str.length()-1));
graph[u].push_back(Edge(u,v,1)); //这里存两次,一次正向,一次反向,方便求解maxlen和dp,一开始不能确定父节点
graph[v].push_back(Edge(v,u,2));
}else if(str.back()=='u'){
v=stoi(str.substr(0,str.length()-1));
graph[u].push_back(Edge(u,v,2));
graph[v].push_back(Edge(v,u,1));
}else{
v=stoi(str);
graph[u].push_back(Edge(u,v,0));//此处方便
graph[v].push_back(Edge(v,u,0));
}
++n;
}
}
root=1;
++n;
return n!=1;
}
void init() {
//确定初始值
fill(f,f+maxn,Inf);
fill(g,g+maxn,Inf);
for(int i=0;i<=n;++i){
for(int j=0;j<=n;++j)
dp[i][j]=Inf;
}
}
int dfs(int u){
int ans=0;
for(int i=0;i<graph[u].size();++i){
if(graph[u][i].t==1)
ans=max(ans,dfs(graph[u][i].v)+1);
}
return ans;
}
//和方法一相比,主要是这个地方不同,方法一使用了f和g计算了状态变化,同时记录了一个点的2个状态,方法2需要使用dp数组
bool DP(int u,int pa){
if(graph[u].empty()){
f[u]=g[u]=0;
return true;
}
//fu和gu是u的有向边子儿子节点中的最大f值和最大g值,fu2和gu2是u的所有子儿子节点中的最大f值和最大g值
int fu=0,gu=0,fu2=0,gu2=0;
vector<UndirectedEdge> undirectedSons;
for(int i=0;i<graph[u].size();++i){
Edge temp=graph[u][i];
if(temp.v==pa) continue;
if(!DP(temp.v,u)) return false;
if(temp.t==1){
gu=max(gu,g[temp.v]+1);
}else if(temp.t==2){
fu=max(fu,f[temp.v]+1);
}else{
undirectedSons.push_back(UndirectedEdge(temp.v,f[temp.v],g[temp.v]));
}
}
if(undirectedSons.empty()){
dp[u][fu]=min(dp[u][fu],gu);
f[u]=fu; g[u]=gu;
return f[u]+g[u]<=maxlen; //f[u]<Inf说明,fu+gu<=maxlen,说明最长链没有超过k,返回true
}
// f[u]=g[u]=Inf;
int s=undirectedSons.size();
sort(undirectedSons.begin(),undirectedSons.end(),cmpF);
int maxg[maxn];
maxg[s-1]=undirectedSons[s-1].g;
for(int k=s-2;k>=0;--k){
maxg[k]=max(maxg[k+1],undirectedSons[k].g);
}
//这地方注意,k代表选择u->undirectedSons[k].w方向边的数量,k和k之前的边u->i,k之后的i->u
for(int k=0;k<=s;++k){
fu2=fu; gu2=gu; //还未划分方向
if(k>0) fu2=max(fu,undirectedSons[k-1].f+1);
if(k<s) gu2=max(gu,maxg[k]+1);
dp[u][fu2]=min(dp[u][fu2],gu2);
}
for(int i=0;i<=n;++i){
if(dp[u][i]+i<=maxlen){
f[u]=min(f[u],i);
g[u]=min(g[u],dp[u][i]);
}
}
for(int i=0;i<=n;++i){
if(dp[u][i]+i<=maxlen) return true;
}
return false;
}
int main(void){
while(readData()){
maxlen=0; //maxlen为删去无向边最长链边数
for(int i=1;i<=n;++i){
maxlen=max(maxlen,dfs(i));
}
init();
//本题中链长为节点数+1
if(DP(root,-1)){
printf("%d\n",maxlen+1);
}else printf("%d\n",maxlen+2);
}
return 0;
}