题目大意:国家有n个城市,还有n条道路,每条道路连通两个不同的城市,n条道路使得所有n个城市相互连通。现在国家经费不足,要关闭一条道路。国家的不便度定义为国家中任意两个不同的城市之间的距离的最大值,那么求选择关闭哪一条道路能使得国家的不便度最小。
首先说明一下将城市看作图中的顶点,而道路则是连接各个顶点的边,因此整个国家可以看作一副图。并且由提供的信息知道这是一副连通图,而n顶点n边的连通图必然是存在环路的,只有移除环路上的边才能使得图保持连通,详情参阅我的另外一篇博客《连通图的一些性质》。
显然最优结果是移除环路上的边,因为移除其它边只会破坏图的连通性质,从而使得国家的不便度达到无穷(存在不连通城市)。那么具体该选择哪条边呢?
可以将整个图理解为一个环路,每个环路上的顶点上挂坠一个子连通图,不同顶点挂坠必须通过环路才能连通的不同子连通图,被挂坠的顶点属于挂坠在其上的子连通图,称这若干副连通图为孤立连通图,若其挂坠在环路上的结点V上,则称为V代表的孤立连通图。在这种情况下,最远的两个结点必然来自两幅不同或相同的孤立连通图。
由于从环中移除边,不会影响来自同一孤立连通图的顶点之间的距离,因此每个孤立连通图之间的最远的两个顶点可以率先计算出来。这个过程可以通过《连通图的一些性质》中命题4部分在O(n)时间复杂度内完成。
而对于不同孤立连通图的顶点,需要先计算每个环上结点V,与V代表的孤立连通图中顶点的最远距离,记作V.END,这个操作可以合并到上一步中,并且不会影响上一步的时间复杂度。而对于任意两个环上结点V,U,来自两个结点所代表孤立连通图的结点的距离最大值应该为WD(V,U)=V.END+U.END+V与U的距离D(V,U)。
将环写作以下顶点的序列V[1], V[2], ... , V[k],并假设其沿着顺时针(或逆时针)进行排序。记L(v, u)表示连接v和u的边的长度,接下来我们记E[i]=L(V[i], V[i+1]),其中i=1,..., k - 1,而E[k] = L(V[k], V[1])。这些操作在O(n)的时间可以完成。
记LEFT[i]表示在移除边(V[i], V[i+1])的前提下,V[1], ... , V[i]表示所代表的孤立连通图中最远的两个顶点的距离;而RIGHT[i]表示在移除边(V[i], V[i+1])的前提下,V[i+1], ... , V[k]所代表的孤立连通图中最远的两个顶点的距离;记CROSS[i]表示在移除边(V[i], V[i+1])的前提下,V[i+1], ... , V[k]所代表的孤立连通图中某个顶点到V[1], ... , V[i]所代表的孤立连通图中另外一个顶点之间最大的距离。因此当移除边(V[i], V[i+1])时,对于距离最远的来自不同的孤立连通图的两个顶点U, V,必然有
$$ WD\left(V,U\right)=MAX\left(LEFT\left[i\right],RIGHT\left[i\right],CROSS\left[i\right]\right) $$在已知LEFT,RIGHT,CROSS的情况下这部分总的时间复杂度为O(1)*n=O(n)。
说一下如果利用动态规划在O(n)时间复杂度内计算LEFT,RIGHT和CROSS。记每个顶点V[i]都维护了属性V[i].S=V[1].S+V[2].S+...V[i-1].S。维护数组W[i]=V[i].END+V.S,M[i]=V[i].END-V[i].S,LW[i]=MAX(W[1], W[2], ... , W[i-1]),LM[i]=MAX(M[1], M[2], ... ,M[i-1]), RW[i] = MAX(W[i+1], W[i+2], ... , W[k]),RM[i] = MAX(M[i+1], M[i+2], ... , M[k]),这可以在O(n)的时间复杂度内完成。
在移除边(V[i], V[i+1])的前提下,LEFT[i]值对应的两个顶点来自环上的顶点A,B所代表的孤立连通图,A,B中或者一者为V[i],或者二者均不是V[i]。在后面的情况下LEFT[i]=LEFT[i-1]。而在前面的情况下不妨设B=V[i],则WD(A,V[i])=A.END+V[i].END+D(A,V[i])=(A.END-A.S)+(V[i].END+V[i].S)=LM[i] + W[i]。即LEFT[i]=MAX(LEFT[i-1], LM[i] + W[i])
同样RIGHT[i]或者为RIGHT[i+1],或者为WD(V[i+1], B)=(V[i+1].END-V[i+1].S) + (B.END + B.S)=M[i+1] + RW[i+1]。即RIGHT[i]=MAX(RIGHT[i+1], M[i+1] + RW[i+1])。
而CROSS[i]=WD(A,B)=A.END+B.END+D(A,B)=A.END+B.END+(V[k].S+E[k]+B.S-A.S)=(B.S+B.END)+(A.END-A.S)+E[k]+V[k].S=RM[i]+LW[i+1]+E[k]+V[k].S。
因此整个DP的过程都可以在O(n)的时间复杂度内完成。
最后提供AC的JAVA代码:
package cn.dalt.codeforces; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** * Created by dalt on 2017/9/6. */ public class RoadsInTheKingdom2 { private static final long INFINITE_SMALL = (long) (-1e14); int n; Node[] allNodes; LinkedList<Node> loop = new LinkedList<>(); LinkedList<Long> distanceToNextNode = new LinkedList<>(); public static void main(String[] args) throws Exception { RoadsInTheKingdom2 instance = new RoadsInTheKingdom2(); instance.init(new AcmInputReader(System.in)); long result = instance.solve(); System.out.println(result); } void init(AcmInputReader input) throws Exception { n = input.nextInteger(); allNodes = new Node[n]; for (int i = 0; i < n; i++) { allNodes[i] = new Node(); allNodes[i].id = i + 1; } for (int i = 0; i < n; i++) { int ui = input.nextInteger() - 1; int vi = input.nextInteger() - 1; int li = input.nextInteger(); allNodes[ui].addNearNode(allNodes[vi], li); allNodes[vi].addNearNode(allNodes[ui], li); } } boolean searchLoop(Node node, Node from) { if (node.flag == Node.FLAG_VISITED) { //find a loop, and (from, node) is an edge in loop //Remove (from, node) long distance = node.removeNearNode(from); from.removeNearNode(node); searchLoop0(node, from, null); loop.add(node); distanceToNextNode.add(distance); node.flag = Node.FLAG_IN_LOOP; return true; } node.flag = Node.FLAG_VISITED; for (Node next : node.nearNodes) { if (next == from) { continue; } if(searchLoop(next, node)) { return true; } } return false; } boolean searchLoop0(Node node, Node target, Node from) { if (node == target) { return true; } Iterator<Long> distanceIterator = node.distanceTo.iterator(); for (Node next : node.nearNodes) { Long distanceTo = distanceIterator.next(); if (next == from) { continue; } if (searchLoop0(next, target, node)) { loop.add(next); distanceToNextNode.add(distanceTo); next.flag = Node.FLAG_IN_LOOP; return true; } } return false; } void preCalculateEndAndMid(Node node, Node from) { Iterator<Long> distanceIterator = node.distanceTo.iterator(); SimpleHeap heap = new SimpleHeap(); long maxMid = 0; for (Node next : node.nearNodes) { Long distanceTo = distanceIterator.next(); if (next.flag == Node.FLAG_IN_LOOP) { continue; } if (next == from) { continue; } preCalculateEndAndMid(next, node); heap.update(distanceTo + next.end); maxMid = Math.max(maxMid, next.mid); } node.end = heap.max; node.mid = Math.max(heap.max + heap.secondaryMax, maxMid); } void preCalculateAloneRelatedGraph() { for (Node node : loop) { preCalculateEndAndMid(node, null); } } long solve() { searchLoop(allNodes[0], null); preCalculateAloneRelatedGraph(); long possibleResult = 0; for (Node node : loop) { possibleResult = possibleResult > node.mid ? possibleResult : node.mid; } //DP part int loopSize = loop.size(); long[] cross = new long[loopSize]; long[] left = new long[loopSize]; long[] right = new long[loopSize]; { long[] w = new long[loopSize]; long[] m = new long[loopSize]; long[] lw = new long[loopSize]; long[] lm = new long[loopSize]; long[] rw = new long[loopSize]; long[] rm = new long[loopSize]; long distanceToFirstVertexInLoop = 0; { int i = 0; Iterator<Long> distanceIterator = distanceToNextNode.iterator(); for (Node node : loop) { w[i] = node.end + distanceToFirstVertexInLoop; m[i] = node.end - distanceToFirstVertexInLoop; distanceToFirstVertexInLoop += distanceIterator.next(); i++; } } lw[0] = INFINITE_SMALL; lm[0] = INFINITE_SMALL; left[0] = INFINITE_SMALL; for (int i = 1; i < loopSize; i++) { lw[i] = Math.max(lw[i - 1], w[i - 1]); lm[i] = Math.max(lm[i - 1], m[i - 1]); left[i] = Math.max(left[i - 1], lm[i] + w[i]); } rw[loopSize - 1] = INFINITE_SMALL; rm[loopSize - 1] = INFINITE_SMALL; right[loopSize - 1] = INFINITE_SMALL; for (int i = loopSize - 2; i >= 0; i--) { rw[i] = Math.max(rw[i + 1], w[i + 1]); rm[i] = Math.max(rm[i + 1], m[i + 1]); right[i] = Math.max(right[i + 1], rw[i + 1] + m[i + 1]); } for (int i = 0, bound = loopSize - 1; i < bound; i++) { cross[i] = rm[i] + lw[i + 1] + distanceToFirstVertexInLoop; } } //Find the best solution with DP results long minInconvenienceCrossLoop = left[loopSize - 1]; for (int i = 0, bound = loopSize - 1; i < bound; i++) { long inconvenience = 0; inconvenience = Math.max(left[i], inconvenience); inconvenience = Math.max(right[i], inconvenience); inconvenience = Math.max(cross[i], inconvenience); minInconvenienceCrossLoop = Math.min(minInconvenienceCrossLoop, inconvenience); } return Math.max(minInconvenienceCrossLoop, possibleResult); } private static class SimpleHeap { long max = 0; long secondaryMax = 0; void update(long value) { if (value > max) { secondaryMax = max; max = value; } else if (value > secondaryMax) { secondaryMax = value; } } } private static class Node { public static final int FLAG_VISITED = 1; public static final int FLAG_IN_LOOP = 2; List<Node> nearNodes = new LinkedList<>(); List<Long> distanceTo = new LinkedList<>(); long end; long mid; int flag; int id; @Override public String toString() { return "" + id; } void addNearNode(Node node, long distance) { nearNodes.add(node); distanceTo.add(distance); } long distanceTo(Node node) { Iterator<Node> nodeIterator = nearNodes.iterator(); Iterator<Long> distanceIteartor = distanceTo.iterator(); while (nodeIterator.next() != node) { distanceIteartor.next(); } return distanceIteartor.next(); } long removeNearNode(Node node) { Iterator<Node> nodeIterator = nearNodes.iterator(); Iterator<Long> distanceIteartor = distanceTo.iterator(); while (nodeIterator.next() != node) { distanceIteartor.next(); } nodeIterator.remove(); long result = distanceIteartor.next(); distanceIteartor.remove(); return result; } } /** * @author dalt * @see java.lang.AutoCloseable * @since java1.7 */ private static class AcmInputReader implements AutoCloseable { private PushbackInputStream in; /** * 创建读取器 * * @param input 输入流 */ public AcmInputReader(InputStream input) { in = new PushbackInputStream(new BufferedInputStream(input)); } @Override public void close() throws IOException { in.close(); } private int nextByte() throws IOException { return in.read() & 0xff; } /** * 如果下一个字节为b,则跳过该字节 * * @param b 被跳过的字节值 * @throws IOException if 输入流读取错误 */ public void skipByte(int b) throws IOException { int c; if ((c = nextByte()) != b) { in.unread(c); } } /** * 如果后续k个字节均为b,则跳过k个字节。这里{@literal k<times} * * @param b 被跳过的字节值 * @param times 跳过次数,-1表示无穷 * @throws IOException if 输入流读取错误 */ public void skipByte(int b, int times) throws IOException { int c; while ((c = nextByte()) == b && times > 0) { times--; } if (c != b) { in.unread(c); } } /** * 类似于{@link #skipByte(int, int)}, 但是会跳过中间出现的空白字符。 * * @param b 被跳过的字节值 * @param times 跳过次数,-1表示无穷 * @throws IOException if 输入流读取错误 */ public void skipBlankAndByte(int b, int times) throws IOException { int c; skipBlank(); while ((c = nextByte()) == b && times > 0) { times--; skipBlank(); } if (c != b) { in.unread(c); } } /** * 读取下一块不含空白字符的字符块 * * @return 下一块不含空白字符的字符块 * @throws IOException if 输入流读取错误 */ public String nextBlock() throws IOException { skipBlank(); StringBuilder sb = new StringBuilder(); int c = nextByte(); while (AsciiMarksLazyHolder.asciiMarks[c = nextByte()] != AsciiMarksLazyHolder.BLANK_MARK) { sb.append((char) c); } in.unread(c); return sb.toString(); } /** * 跳过输入流中后续空白字符 * * @throws IOException if 输入流读取错误 */ private void skipBlank() throws IOException { int c; while ((c = nextByte()) <= 32) ; in.unread(c); } /** * 读取下一个整数(可正可负),这里没有对溢出做判断 * * @return 下一个整数值 * @throws IOException if 输入流读取错误 */ public int nextInteger() throws IOException { skipBlank(); int value = 0; boolean positive = true; int c = nextByte(); if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { positive = c == '+'; } else { value = '0' - c; } c = nextByte(); while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { value = (value << 3) + (value << 1) + '0' - c; c = nextByte(); } in.unread(c); return positive ? -value : value; } /** * 判断是否到了文件结尾 * * @return true如果到了文件结尾,否则false * @throws IOException if 输入流读取错误 */ public boolean isMeetEOF() throws IOException { int c = nextByte(); if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.EOF) { return true; } in.unread(c); return false; } /** * 判断是否在跳过空白字符后抵达文件结尾 * * @return true如果到了文件结尾,否则false * @throws IOException if 输入流读取错误 */ public boolean isMeetBlankAndEOF() throws IOException { skipBlank(); int c = nextByte(); if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.EOF) { return true; } in.unread(c); return false; } /** * 获取下一个用英文字母组成的单词 * * @return 下一个用英文字母组成的单词 */ public String nextWord() throws IOException { StringBuilder sb = new StringBuilder(16); skipBlank(); int c; while ((AsciiMarksLazyHolder.asciiMarks[(c = nextByte())] & AsciiMarksLazyHolder.LETTER_MARK) != 0) { sb.append((char) c); } in.unread(c); return sb.toString(); } /** * 读取下一个长整数(可正可负),这里没有对溢出做判断 * * @return 下一个长整数值 * @throws IOException if 输入流读取错误 */ public long nextLong() throws IOException { skipBlank(); long value = 0; boolean positive = true; int c = nextByte(); if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { positive = c == '+'; } else { value = '0' - c; } c = nextByte(); while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { value = (value << 3) + (value << 1) + '0' - c; c = nextByte(); } in.unread(c); return positive ? -value : value; } /** * 读取下一个浮点数(可正可负),浮点数是近似值 * * @return 下一个浮点数值 * @throws IOException if 输入流读取错误 */ public float nextFloat() throws IOException { return (float) nextDouble(); } /** * 读取下一个浮点数(可正可负),浮点数是近似值 * * @return 下一个浮点数值 * @throws IOException if 输入流读取错误 */ public double nextDouble() throws IOException { skipBlank(); double value = 0; boolean positive = true; int c = nextByte(); if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { positive = c == '+'; } else { value = c - '0'; } c = nextByte(); while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { value = value * 10.0 + c - '0'; c = nextByte(); } if (c == '.') { double littlePart = 0; double base = 1; c = nextByte(); while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { littlePart = littlePart * 10.0 + c - '0'; base *= 10.0; c = nextByte(); } value += littlePart / base; } in.unread(c); return positive ? value : -value; } /** * 读取下一个高精度数值 * * @return 下一个高精度数值 * @throws IOException if 输入流读取错误 */ public BigDecimal nextDecimal() throws IOException { skipBlank(); StringBuilder sb = new StringBuilder(); sb.append((char) nextByte()); int c = nextByte(); while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { sb.append((char) c); c = nextByte(); } if (c == '.') { sb.append('.'); c = nextByte(); while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { sb.append((char) c); c = nextByte(); } } in.unread(c); return new BigDecimal(sb.toString()); } /** * 读取下一个大整数数值 * * @return 下一个大整数数值 * @throws IOException if 输入流读取错误 */ public BigInteger nextBigInteger() throws IOException { skipBlank(); StringBuilder sb = new StringBuilder(); sb.append((char) nextByte()); int c = nextByte(); while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { sb.append((char) c); c = nextByte(); } in.unread(c); return new BigInteger(sb.toString()); } private static class AsciiMarksLazyHolder { public static final byte BLANK_MARK = 1; public static final byte SIGN_MARK = 1 << 1; public static final byte NUMERAL_MARK = 1 << 2; public static final byte UPPERCASE_LETTER_MARK = 1 << 3; public static final byte LOWERCASE_LETTER_MARK = 1 << 4; public static final byte LETTER_MARK = UPPERCASE_LETTER_MARK | LOWERCASE_LETTER_MARK; public static final byte EOF = 1 << 5; public static byte[] asciiMarks = new byte[256]; static { for (int i = 0; i <= 32; i++) { asciiMarks[i] = BLANK_MARK; } asciiMarks['+'] = SIGN_MARK; asciiMarks['-'] = SIGN_MARK; for (int i = '0'; i <= '9'; i++) { asciiMarks[i] = NUMERAL_MARK; } for (int i = 'a'; i <= 'z'; i++) { asciiMarks[i] = LOWERCASE_LETTER_MARK; } for (int i = 'A'; i <= 'Z'; i++) { asciiMarks[i] = UPPERCASE_LETTER_MARK; } asciiMarks[0xff] = EOF; } } } }