动态规划学习(1)

《算法竞赛入门到进阶》学习。

《算法竞赛入门到进阶》

1. 动态规划的概念和思想

DP(Dynamic Programming)是一种算法思想,不是一个特定的算法。

DP与分治法的区别:

  • 分治法是将问题分成独立的子问题,每个子问题能独立解决
  • DP的子问题是相关的,前面子问题的解决结果被后面的子问题使用。

求解DP有 $3$ 步,定义状态、状态转移、算法实现。

2. 基础DP

基础DP是一些经典问题,如递推、01背包、最长公共子序列(LCS)、最长递增子序列(LIS)等。

2.1 硬币问题

2.1.1 最少硬币问题

有 n 种硬币,面值分别为 $v_1,v_2,…,v_n$,数量无限。输入非负整数 s,选硬币使其和为 s。要求输出最少硬币组合。

void solve(){
	int S=251,N=5,s;
	int Min[S],v[5]={1,5,10,25,50};
	memset(Min,0x3f,sizeof(Min));
	Min[0]=0;
	for(int j=0;j<N;j++)
		for(int i=v[j];i<S;i++)
			Min[i]=min(Min[i],Min[i-v[j]]+1);
	cin>>s;
	cout<<Min[s]<<"\n";
}

上述时间复杂度是 $O(SN)$。

2.1.2 打印最少硬币组合

在DP中,除求最优解的数量外,往往还要求输出最优解本身。

在最少硬币问题中,如果要求打印组合方案,需要增加一个记录表 Min_path[i],记录金额 i 需要的最后一个硬币。利用 Min_path[] 逐步倒推,就能得到所有的硬币。

void prin(int *Min_path,int s){
	while(s){
		cout<<Min_path[s]<<" ";
		s=s-Min_path[s];
	}
}
void solve(){
	int S=251,N=5,s;
	int Min[S],v[5]={1,5,10,25,50};
	int Min_path[S]={0};
	memset(Min,0x3f,sizeof(Min));
	Min[0]=0;
	for(int j=0;j<N;j++)
		for(int i=v[j];i<S;i++){
			if(Min[i]>Min[i-v[j]]+1){
				Min_path[i]=v[i];
				Min[i]=min(Min[i],Min[i-v[j]]+1);
			}
		}
	cin>>s;
	prin(Min_path,s);
}

用数组存储由哪个状态转移而来,即可得到转移路径。

2.1.3 所有硬币组合

有 n 种硬币,面值分别为 $v_1,v_2,…,v_n$,数量无限。输入非负整数 s,选硬币使其和为 s。要求输出所有的硬币组合数量。

如果选取硬币的数量无限:

void solve(){
	int S=251,N=5,s;
	int dp[S]={0},v[5]={1,5,10,25,50};
	dp[0]=1;
	for(int i=0;i<N;i++)
		for(int j=v[i];j<S;j++)
			dp[j]=dp[j]+dp[j-v[i]];
	cin>>s;
	cout<<dp[s]<<"\n";
}

如果选取的硬币数量有限:

void solve(){
	int C=101,M=251;
	int dp[M][C]={0};
	int v[5]={1,5,10,25,50};
	int ans[M]={0};
	dp[0][0]=1;
	for(int i=0;i<5;i++)
		for(int j=1;j<C;j++)
			for(int k=v[i];k<M;k++)
				dp[k][j]+=dp[k-v[i]][j-1];
	for(int i=0;i<M;i++)
		for(int j=0;j<C;j++)
			ans[i]+=dp[i][j];
	int s;
	while(cin>>s)
		cout<<ans[s]<<"\n";
}

2.2 01背包问题

约束条件: \(\sum_{i=1}^{n} w_ix_i≤C ~,~ x_i∈\{0,1\},1≤i≤n\) 目标函数: \(max\sum_{i=1}^{n}v_ix_i\) 基础代码如下:

void solve(){
	int n,m;
	cin>>n>>m;
	int w[n],v[n],dp[n][m+1]={0};
	for(int i=0;i<n;i++) cin>>w[i];
	for(int i=0;i<n;i++) cin>>v[i];
	for(int i=0;i<n;i++){
		for(int j=0;j<=m;j++){
			if(j<v[i]) dp[i][j]=dp[i-1][j];
			else dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
		}
	}
	cout<<dp[n][m]<<"\n";
}

滚动数组覆盖了中间转移状态,只留下了最后的状态,所有损失了很多信息,导致无法输出背包的方案。

滚动数组优化:

void solve(){
	int n,m;
	cin>>n>>m;
	int w[n],v[n],dp[m+1]={0};
	for(int i=0;i<n;i++) cin>>w[i];
	for(int i=0;i<n;i++) cin>>v[i];
	for(int i=0;i<n;i++){
		for(int j=m;j>=v[i];j--){
			dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
		}
	}
	cout<<dp[m]<<"\n";
}

2.3 最长公共子序列(LCS)

最长公共子序列(Longest Common Subsequence)问题:给定两个序列 X 和 Y,找出 X 和 Y 的一个最长公共子序列。

用 $dp[i][j]$ 表示子序列 \(X_i\) 和 \(Y_j\) 的最长公共子序列长度。

当 $X_i=Y_i$ 时,找出 \(X_{i-1}\) 和 \(Y_{i-1}\) 的最长公共子序列,然后在其尾部加上 $X_i$ 即可得到 \(X\) 和 \(Y\) 的最长公共子序列。

当 \(X_i≠Y_i\) 时,最长公共子序列就是 \([X_{i-1},Y_j]\) 的LCS和 \([X_i,Y_{j-1}]\) 的LCS二者中最大值。

void solve(){
	string s1,s2;
	cin>>s1>>s2;
	int dp[s1.size()+1][s2.size()+1]={0};
	for(int i=1;i<=s1.size();i++)
		for(int j=1;j<=s2.size();j++){
			if(s1[i-1]==s2[j-1]) dp[i][j]=dp[i-1][j-1]+1;
			else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
		}
	cout<<dp[s1.size()][s2.size()]<<"\n";
	return ;
}

滚动数组优化:

void solve(){
	string s1,s2;
	cin>>s1>>s2;
	int dp[2][s2.size()+1];
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=s1.size();i++)
		for(int j=1;j<=s2.size();j++){
			if(s1[i-1]==s2[j-1]) dp[i%2][j]=dp[(i-1)%2][j-1]+1;
			else dp[i%2][j]=max(dp[(i-1)%2][j],dp[i%2][j-1]);
		}
	cout<<dp[s1.size()%2][s2.size()]<<"\n";
	return ;
}

这里不能定义 \(int~dp[2][s2.size()+1]=\{0\}\) ,这样dp数组为变长数组(VLA),VLA是C99特性,不是标准C++的一部分,某些编译器(如GCC扩展)可能无法正确初始化。

2.4 最长递增子序列(LIS)

最长递增子序列(Longest Increasing Subsequence)。

第一种方法,将序列排序后与原序列求最长公共子序列即为LIS,复杂度为 \(O(N^2)\)。

void solve(){
	int n;cin>>n;
	int a[n],b[n],dp[2][n+1];
	for(int i=0;i<n;i++) cin>>a[i],b[i]=a[i];
	sort(b,b+n);
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++){
			if(a[i-1]==b[j-1]) dp[i%2][j]=dp[(i-1)%2][j-1]+1;
			else dp[i%2][j]=max(dp[(i-1)%2][j],dp[i%2][j-1]);
		}
	cout<<dp[n%2][n]<<"\n";
}

第二种方法,直接DP求解,状态 \(dp[i]\) 表示以第 \(i\) 个数为结尾的LIS长度,状态转移方程为 \(dp[i]=max\{0,dp[j]\}+1,~0<j<i,A_j<A_i\) ,

答案为 \(max\{dp[i]\}\),复杂度为 \(O(N^2)\) 。

void solve(){
	int n,ans=0;cin>>n;
	int a[n],dp[n+1];
	for(int i=0;i<n;i++) cin>>a[i];
	memset(dp,0,sizeof(dp));
	for(int i=0;i<n;i++){
		dp[i]=1;
		for(int j=0;j<i;j++)
			if(a[j]<a[i]) dp[i]=max(dp[i],dp[j]+1);
	}
	for(int i=0;i<n;i++) ans=max(ans,dp[i]);
	cout<<ans<<"\n";
}

第三种方法,单调队列,复杂度为 \(O(nlogn)\)。

void solve(){
	int n,ans=0;cin>>n;
	int a[n];
	for(int i=0;i<n;i++) cin>>a[i];
	vector<int> v;
	v.push_back(a[0]);
	for(int i=1;i<n;i++){
		int x=upper_bound(v.begin(),v.end(),a[i])-v.begin();
		if(x==v.size()) v.push_back(a[i]);
		else v[x]=a[i];
	}
	cout<<v.size()<<"\n";
}

3. 递推与记忆化搜索

记忆化搜索,在用递归实现DP时,在递归程序中记录计算过的状态,并在后续的计算中跳过已经算过的重复状态。

在很多情况下,记忆化搜索的逻辑思路和程序比直接写递推更简单。

4.区间DP

区间DP的主要思想是现在小区间进行DP得到最优解,再利用小区间的最优解合并求大区间的最优解。

区间DP的两个难点,枚举所有可能的区间、状态转移方程。

下面用两个经典问题讲解:

4.1 石子合并问题

设有 $N$堆石子排成一排,其编号为 $1,2,3,\cdots,N$。每堆石子有一定的质量 $m_i$。现在要将这 $N$ 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。输出最小代价。

状态:\(DP[i][j]\) 为从第 \(i\) 堆石子到第 \(j\) 堆石子的最小花费。

转移方程:\(dp[i][j]=min(dp[i][k]+dp[k+1][j])+sum[i][j],i≤k≤j\)

void solve(){
	int n;cin>>n;
	int m[N],dp[N][N],sum[N];
	sum[0]=0;
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++) 
		cin>>m[i],sum[i]=sum[i-1]+m[i];
	for(int len=1;len<n;len++){
		for(int i=1;i+len<=n;i++){
			int j=i+len;
			dp[i][j]=0x3f3f3f3f;
			for(int k=i;k<=j;k++){
				dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
			}
		}
	}
	cout<<dp[1][n]<<"\n";
}

时间复杂度为 \(O(n^3)\),只能用来处理规模 \(n<250\) 的问题。

上述三重循环中,前两重循环是枚举所有可能的合并,无法优化,最后一重循环枚举分割点 \(k\) ,是可以优化的。因为每次运行最后一层循环时,都在某个子区间内部寻找最优分割点,该操作在多个子区间是重复的。如果找到这个最优点后保存下来,用于下一次循环,就能避免重复计算。

用 \(s[i][j]\) 表示区间 \([i,j]\) 中的最优分割点,第三重循环可以从区间 \([i,j-1)\) 的枚举优化到区间 \([s[i][j-1],s[i+1][j]]\) 的枚举。其中, \(s[][]\) 的值是在前面的三重循环中找到并记录下来的。符合 “平行四边形优化” 的原理。

void solve(){
	int n;cin>>n;
	int m[N],dp[N][N],sum[N],s[N][N];
	sum[0]=0;
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++) 
		cin>>m[i],sum[i]=sum[i-1]+m[i],s[i][i]=i;
	for(int len=1;len<n;len++){
		for(int i=1;i+len<=n;i++){
			int j=i+len;
			dp[i][j]=0x3f3f3f3f;
			for(int k=s[i][j-1];k<=s[i+1][j];k++){
				if(dp[i][j]>dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]){
					dp[i][j]=dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1];
					s[i][j]=k;
				}
			}
		}
	}
	cout<<dp[1][n]<<"\n";
}

优化后,复杂度接近 \(O(n^2)\) ,可以解决 \(n<3000\) 的问题。

4.2 回文串问题

回文串问题:给定一个字符串,然后通过增加和删除部分字符串得到一个回文串,每个字母每种操作代价不同,求最小代价。

定义状态 \(dp[i][j]\) 表示字符串 \(s\) 的子区间 \(s[i,j]\) 变成回文的最小花费。

void solve(){
	int n,m,w[26];
	cin>>m>>n;
	string s;
	cin>>s;s=" "+s;
	while(m--){
		char c;int l,r;
		cin>>c>>l>>r;
		w[c-'a']=min(l,r);
	}
	int dp[N][N]={0};
	for(int len=1;len<n;len++){
		for(int i=1;i<=n-len;i++){
			int j=i+len;
			if(s[i]==s[j]){
				dp[i][j]=dp[i+1][j-1];
			}else dp[i][j]=min(dp[i+1][j]+w[s[i]-'a'],dp[i][j-1]+w[s[j]-'a']);
		}
	}
	cout<<dp[1][n]<<"\n";
}

5. 树形DP

树本身具有 “子结构” 性质,具有递归性,符合DP的性质。

例题1:不相邻最大权值

一颗有根树上的每个结点有一个权值,相邻的父结点和子结点只能选择一个,如何选择使总权值之和最大。

状态:

  • \(dp[i][0]\) 表示不选择 \(i\) 结点时最优解;

  • \(dp[i][1]\) 表示选择 \(i\) 结点时最优解。

转移方程:

  • \[dp[i][0]+=max(dp[son][1],dp[son][0])\]
  • \[dp[i][1]+=dp[son][0]\]
  • 以上 \(son\),表示子结点,兄弟关系无限制,所以是兄弟求和
#include<bits/stdc++.h>
using namespace std;
const int N=6e3+5;
int value[N],dp[N][2],father[N],n;
vector<int> tree[N];//邻接表
void dfs(int u){
	dp[u][1]=value[u];
	for(int i=0;i<tree[u].size();i++){
		int son=tree[u][i];
		dfs(son);
		dp[u][0]+=max(dp[son][0],dp[son][1]);
		dp[u][1]+=dp[son][0];
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>value[i];
	memset(father,-1,sizeof(father));
	for(int i=1;i<n;i++){
		int a,b;cin>>a>>b;
		tree[b].push_back(a);
		father[a]=b;
	}
	int t=1;
	while(father[t]!=-1) t=father[t];//查找根节点
	dfs(t);
	cout<<max(dp[t][0],dp[t][1])<<"\n";
	return 0;
}

上述遍历每一个结点,复杂度为 \(O(n)\)。

例题2:最远距离

一棵有根树,根节点为 1 ,对其中任意个结点,求最远结点的距离。

状态:

  • \(dp[i][0]\) :结点 i 的子树到 i 的最长距离
  • \(dp[i][1]\) :结点 i 的子树到 i 的次长距离
  • \(dp[i][2]\) :结点 i 往上父节点走的最长距离
#include<bits/stdc++.h>
using namespace std;
const int N=10100;;
struct Node{
	int id;//结点编号
	int cost;//第i结点连接到id结点的边权
};
vector<Node> tree[N];//邻接表
int dp[N][3];
int n;
void init_read(){
	for(int i=1;i<=n;i++) tree[i].clear();
	memset(dp,0,sizeof(dp));
	for(int i=2;i<=n;i++){
		int x,y;cin>>x>>y;
		Node tmp;
		tmp.id=i,tmp.cost=y;//i是x的子结点
		tree[x].push_back(tmp);
	}
}
void dfs1(int u){//dfs,先处理子结点,再处理父结点
	int one=0,two=0;
	for(int i=0;i<tree[u].size();i++){
		//遍历结点father的所有子结点
		Node child=tree[u][i];
		dfs1(child.id);//递归子结点
		int cost=dp[child.id][0]+child.cost;
		//更新最长距离和次长距离
		if(cost>=one){
			two=one;
			one=cost;
		}
		if(cost<one&&cost>two) two=cost;
	}
	dp[u][0]=one;
	dp[u][1]=two;
}
void dfs2(int u){//先处理父结点,再处理子结点
	for(int i=0;i<tree[u].size();i++){
		Node child=tree[u][i];
		if(dp[child.id][0]+child.cost==dp[u][0])//child在最长距离子树上
			dp[child.id][2]=max(dp[u][2],dp[u][1])+child.cost;
		else dp[child.id][2]=max(dp[u][2],dp[u][0])+child.cost;
		dfs2(child.id);
	}
}
int main(){
	while(cin>>n){
		init_read();//初始化,读入
		dfs1(1);//计算dp[][0],dp[][1]
		dp[1][2]=0;
		dfs2(1);//计算dp[][2]
		for(int i=1;i<=n;i++)
			cout<<max(dp[i][0],dp[i][2])<<"\n";
	}
}

6. 数位DP

数位DP:对数字的 “位” 进行与计数有关的DP。

在数位DP中,一般可以使用模板来做这种题型。

状态:\(dp[i][j]\) 表示第 \(i\) 位数中为 \(j\) 的符合要求的数个数

转移方程:\(dp[i][j]= \sum_{k=0}^{9} dp[i-1][k],特殊条件\)

处理完DP数组后,根据给定的范围求答案。

如一个六位数 362415=000000~099999+100000~199999+200000~299999+300000~362415

\[ans=dp[6][0]+dp[6][1]+dp[6][2]+300000 \sim 362415\]

其中 300000~362415又可以看成00000~62415,但是涉及后一位的时候也需要特判。

例题1:不含62和4

在区间 \([m,n]\) 中,不含 62 和 4 的数字个数。

按照上面模板,我们只需要特判第 i 位为 4 或者第 i 位为 6 且上一位为 2 时continue就行了。

#include<bits/stdc++.h>
using namespace std;
int dp[100][10];
void init(){
	dp[0][0]=1;//初始化
	for(int i=1;i<=7;i++){//位数
		for(int j=0;j<=9;j++){//第i位值为j,后面全为0
			for(int k=0;k<=9;k++){//枚举上一位的所有可能值
				if(j==4||j==6&&k==2) continue;
				dp[i][j]+=dp[i-1][k];
			}
		}
	}
}
int solve(int x){//求1~k中有多少符合的数
	int len=0,digit[10],ans=0;
	memset(digit,0,sizeof(digit));
	while(x){
		digit[++len]=x%10;x/=10;
	}
	for(int i=len;i>=1;i--){
		for(int j=0;j<digit[i];j++){
			if(j==4||j==2&&digit[i+1]==6) continue;
			ans+=dp[i][j];
		}
		if(digit[i]==4||digit[i]==2&&digit[i+1]==6) break;
	}
	return ans;
}
int main(){
	int n,m;
	init();
	while(cin>>n>>m){
		if(n==0&&m==0) break;
		cout<<solve(m+1)-solve(n)<<"\n";
	}
	return 0;
}

7.状态压缩DP

每个状态 \(dp[i][j]\) 表示的不是一个数值如花费长度等,而是代表集合的数量,这种处理复杂集合问题的DP叫做状态压缩DP。

集合的状态有很多,往往把方案用二进制数表示,即压缩成二进制。

例题1:Corn Fields G

在一块长方形的土地上,有的格子能种,有的不能种,选择格子种草,草不会相邻,有多少种方案?

状态:\(dp[i][j]\)表示在第 \(i\) 行状态为 \(j\) 的方案数

\(f\) 数组表示DP数组,\(g\) 数组判断状态是否合法,\(F\) 数组表示输入每行可种植状态。9C0E37D6

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int p=100000000;
ll f[13][1<<12],n,m;
ll g[1<<12],a[13][13];
ll F[13];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			cin>>a[i][j];
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			F[i]=(F[i]<<1)+a[i][j];//第i行能种的格子
	for(int i=0;i<(1<<m);i++){//枚举所有状态
		if(!(i&(i>>1))&&!(i&(i<<1))){//第i位左右都没有1
			g[i]=1;
			if((i&F[1])==i) f[1][i]=1;//是第一行的某状态
		}
	}
	for(int x=2;x<=n;x++)
		for(int j=0;j<(1<<m);j++)
			if(((j&F[x-1])==j)&&g[j])//状态可以在上一行&&可行
				for(int k=0;k<(1<<m);k++)
					if(((k&F[x])==k)&&!(j&k)&&g[k]){//状态可以在当行&&与上状态不相邻&&可行
						f[x][k]=(f[x][k]+f[x-1][j])%p;
					}
	int ans=0;
	for(int i=0;i<(1<<m);i++)
		ans=(ans+f[n][i])%p;
	cout<<ans;
	return 0;
}

例题2:旅行商问题(TSP)

有 n 个城市,知道任何两个城市之间的距离,从某城市出发经过每个城市且只经过一次,最后回到出发城市的最少花费。

这是NP难度的,没有多项式时间的高效算法。

状态:

  • \[S:已经访问过的城市集合\]
  • \[v:当前所在城市\]
  • \[dp[S][v]:从v出发访问所有未访问城市且回到起点的最小花费\]

状态转移:

  • \[dp[V][0]=0,V是最后一个城市\]
  • \[dp[S][v]=min(dp[S \cup {u}][u]+dist(v,u)) ,u \notin S\]
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,c[20][20],f[1<<20][20];
//f[城市经过状态][当前所在城市]=从当前城市出发经过未经过城市的费用最小
int main(){
	cin>>n;
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++)
			cin>>c[i][j];
	memset(f,0x3f,sizeof(f));//将dp数组赋值很大
	f[(1<<n)-1][0]=0;//全部节点都到达过
	for(int s=(1<<n)-2;s>=0;s--){
		for(int v=0;v<n;v++){
			//枚举当前集合,售货员所在位置
			for(int u=0;u<n;u++){
				if((s>>u)&1) continue;//已经到过u节点
				f[s][v]=min(f[s][v],f[s|(1<<u)][u]+c[v][u]);
				//经过u节点
			}
		}
	}
	cout<<f[0][0];
	return 0;
}

例题3:TSP变形

访问 n 个城市,可以从任意城市开始,每个城市访问的次数不少于1次,不多于2次。n 个城市之间有m条道路,每条道路有花费。最少费用是多少。

在普通TSP中,只有访问和不访问两种状态,这个题有3种情况,可以使用三进制压缩状态。

在下面代码中,\(tri[i][j]\) 表示第 i 个路径,其第 j 位的值是城市状态。

状态:

  • \(dp[i][j]\) :当前所在城市 i 出发访问所有剩余城市的后回到起点的最小值。

转移:

\[dp[i][j]=min(dp[i][j],dp[k][l]+graph[k][i])\]
#include<bits/stdc++.h>
using namespace std;
int n,m;
//三进制每位权值
int bit[12]={0,1,3,9,27,81,243,729,2187,6561,19683,59049};
int tri[60000][11];
int dp[11][60000];
int graph[11][11];//存图
void make_trb(){//初始化,所有可能路径
	for(int i=0;i<59050;++i){//共3^10=59049+1种状态
		int t=i;
		for(int j=1;j<=10;++j){
			tri[i][j]=t%3;t/=3;
		}
	}
}
int cmp_dp(){
	int ans=0x3f3f3f3f;
	memset(dp,0x3f,sizeof(dp));
	for(int i=0;i<=n;i++) 
		dp[i][bit[i]]=0;//bit[i]是第i个城市,起点任意
	for(int i=0;i<bit[n+1];i++){
		int flag=1;//所有的城市都遍历过1次以上
		for(int j=1;j<=n;j++){//选一个终点
			if(tri[i][j]==0){//判断终点位是否为0
				flag=0;//还没有经历终点
				continue;
			}
			if(i==j) continue;
			for(int k=1;k<=n;k++){
				int l=i-bit[j];//i状态的第j位置
				if(tri[i][k]==0) continue;
				dp[j][i]=min(dp[j][i],dp[k][l]+graph[k][j]);
			}
		}
		if(flag)//找最小费用
			for(int j=1;j<=n;j++)
				ans=min(ans,dp[j][i]);
	}
	return ans;
}
int main(){
	make_trb();
	while(cin>>n>>m){
		memset(graph,0x3f,sizeof(graph));
		while(m--){
			int a,b,c;cin>>a>>b>>c;
			if(c<graph[a][b]) graph[a][b]=graph[b][a]=c;
		}
		int ans=cmp_dp();
		if(ans==0x3f3f3f3f) cout<<"-1\n";
		else cout<<ans<<"\n";
	}
	return 0;
}