题目大意
迈克尔接下来n天里分别需要支付C[1], C[2], ... , C[n]费用,但是每次支付费用可以选择使用优惠或不使用优惠,每次使用价值X的优惠那么迈克尔所能使用的优惠余量将减少X并且当天所需要支付的费用将减少X,而第一天迈克尔所持有的优惠余量为0。如果不使用优惠,那么优惠余量将增加X/10,其中X是当天迈克尔所支付的费用。
输入规模为1<=n<=3e5,而C[1], ... , C[n]只可能取1000或2000。
思路
下面说明一下我个人的思路:
一个解决方案可以归结为每日所使用的优惠量,即解决方案可以视作一个n维向量,而一个解决方案是有效的,当且仅当到每一天余留的优惠量可以支付当天的优惠使用量。一个有效解决方案S是最优的,当且仅当该解决方案各个维度的加总最大,即SUM(S)=S[1]+...+S[n]最大。
首先可以很简单地证明如下定理:
定理1:对于任意有效的解决方案S,若1<=i<j<=n且C[i]=C[j]且S[i]>0且S[j]<C[i],则可以以特定额度减少第i天使用的优惠量并等额增加第j天使用的优惠量,得到新的解决方案S‘,满足S‘是有效的解决方案且SUM(S‘)>=SUM(S)。
证明:考虑两种情况:若S[j]=0,则可以将额度设置为S[i],否则额度可以设置为1。由于等量转移,因此被第i天所使用的优惠量延迟到第j天使用,显然这样不会违背有效性的定义。而当S[j]=0时,由于额度为S[i],因此到S[j]日结束剩余的优惠量不会发生改变(第i天提供了原本第j天额外提供的优惠量)。
由于最优的解决方案可能会有很多,没有目标的寻找容易丢失方向,接下来就确定要找哪一个特定的解决方案。下面给最优的两个不同解决方案S1,S2引入与字符串比较相同的偏序关系:若S1<S2,当且仅当存在1<=j<=n使得S1[j]<S2[j]且对于任意1<=i<j,满足S1[i]=S2[i]。而我们要找的就是最大的最优解决方案,称之为目标解决方案。
目标解决方案的性质有很多,下面逐一推导。
由定理1可以了解到目标解决方案B必定满足条件:若第i天使用了优惠,那么所有后续的日子j,由C[i]=C[j]能推出B[j]=C[j]。
- 记H2为所有当日需要支付费用为2000的日子中最早使用了优惠的日子,记H1为所有当日需要支付费用为1000的日子中最早使用了优惠的日子。若H2<H1,则B[H1]=1000,若H2>H1,则B[H2]=2000。
- 记H12是H1之后首个费用为1000的日子,若H12<H2,则H12和H2之间不存在费用为2000的日子。
- 若H2<H1,则必定在H2和H1之间不存在两个费用为1000的日子。在前面前提下若C[H2]<=1000,则在H2和H1之间不存在一个费用为1000的日子。
有了上面这些目标解决方案的必要条件,只需要遍历所有满足这些条件的H1和H2的组合,并挑选其中SUM值最大的解决方案,可以保证最终得到的必定是最优解决方案(未必是目标解决方案)。下面是算法的具体流程:
分别尝试H1<H2和H2<H1两种情况,进行线性迭代,寻找SUM值最大的解决方案。
在H1<H2的前提下,对于任意可能的H1,H2必定处于H1,H12之间或H12后首个费用为2000的日子。由于H1最多有n种可能取值,而在H1,H12之间费用为2000的日子与H1的组合数目不会超过n(每一个费用为2000的日子只可能与前一个费用为1000的日子组合),H12后首个费用为2000的日子与H1的组合数也不会超过n,故总共可能的组合数不会超过2n。
在H1>H2的前提下,对于任意可能的H2,则H1可能是H2后前两个费用为2000的日子。由于H1最多有n种可能取值,而每个H1对应两个可能的H2,故总共可能的组合数不会超过2n。
因此总的时间复杂度是O(n)。
上面没有解决在O(1)时间复杂度内判断某个特定的H1, H2组合是否有效。首先开辟一个长度为n的数组R,R[i]记录截至到第i天之前最多能增加的优惠量(即第1,...,i-1日均不使用优惠,所累计的优惠量)。同时开辟一个长度为n的数组A,且A[i]=C[i]+C[i+1]+...+C[n]。依据上面流程优惠使用情况可以综合为两种情况:第一种是从第y天起每天都使用足量优惠抵消当日所有费用,且只有一天x<y使用了优惠。第二种是从第x天起(除了某天y>x)每天都使用优惠,且从x+1天起每一个使用优惠的日子使用的优惠抵消当日费用。要计算两种情况下x天能使用的最大优惠量,可以按照下面的公式计算出来:
第一种情况:$$ allowed=\min\left(C\left[x\right],min\left(R\left[x\right],\,\,R\left[y\right]-A\left[y\right]-\frac{C\left[x\right]}{10}\right)\right) $$
第二种情况:$$ allowed=\min\left(\min\left(R\left[x\right],R\left[x\right]+\frac{C\left[y\right]}{10}-A\left[y+1\right]\right)-\left(A\left[x+1\right]-A\left[y\right]\right),C\left[x\right]\right) $$
其中allowed是允许在第x使用的最大优惠,解决方案无效当且仅当allowed为负数。两种情况在预先计算出R和A的情况下可以以O(1)的时间复杂度计算出来。
代码
下面给出JAVA代码,140ms通过:
1 import java.io.BufferedInputStream; 2 import java.io.IOException; 3 import java.io.InputStream; 4 import java.io.PushbackInputStream; 5 import java.math.BigDecimal; 6 7 /** 8 * Created by Administrator on 2017/9/21. 9 */ 10 public class MichaelAndChargingStations { 11 int totalDay; //The total day number 12 int[] costs; //The cost for each day 13 int[] sumUp; //sumUp[i] = cost[i] + cost[i + 1] + ... + cost[totalDay - 1] 14 int[] remain; //remain[i] = remian[0] + remain[1] + ... + remain[i - 1] 15 16 public static void main(String[] args) { 17 MichaelAndChargingStations solution = new MichaelAndChargingStations(); 18 solution.init(); 19 int result = solution.solve(); 20 System.out.println(result); 21 } 22 23 public void init() { 24 try { 25 AcmInputReader input = new AcmInputReader(System.in); 26 27 totalDay = input.nextInteger(); 28 costs = new int[totalDay]; 29 for (int i = 0; i < totalDay; i++) { 30 costs[i] = input.nextInteger(); 31 } 32 33 } catch (IOException e) { 34 throw new RuntimeException(e); 35 } 36 } 37 38 public int solve() { 39 remain = new int[totalDay]; 40 sumUp = new int[totalDay + 1]; 41 42 remain[0] = 0; 43 for (int i = 0, bound = totalDay - 1; i < bound; i++) { 44 remain[i + 1] = remain[i] + costs[i] / 10; 45 } 46 sumUp[totalDay] = 0; 47 for (int i = totalDay - 1; i >= 0; i--) { 48 sumUp[i] = sumUp[i + 1] + costs[i]; 49 } 50 51 int maxConsume = 0; 52 53 54 int h1, h2; 55 56 //Try H1 < H2 57 h1 = preIndex(1000, totalDay); 58 h2 = totalDay; 59 while (h1 >= 0) { 60 int allowed = maxAllowedUse(h1, h2); 61 if (allowed < 0) { 62 break; 63 } 64 65 maxConsume = Math.max(maxConsume, allowed + sumUp[h2]); 66 67 h2--; 68 if (h2 == h1) { 69 h1 = preIndex(1000, h1); 70 } 71 } 72 73 //Try H2 < H1 74 int h1before = preIndex(1000, totalDay); 75 h2 = preIndex(2000, totalDay); 76 h1 = totalDay; 77 while (h2 >= 0) { 78 if (h1before > h2) { 79 h1 = h1before; 80 h1before = preIndex(1000, h1before); 81 continue; 82 } 83 84 int allowed; 85 allowed = maxAllowedUseWithInterval(h2, h1); 86 if (allowed < 0) { 87 break; 88 } 89 maxConsume = Math.max(maxConsume, allowed + sumOf(h2 + 1, h1) + sumOf(h1 + 1, totalDay)); 90 91 allowed = maxAllowedUseWithInterval(h2, totalDay); 92 if (allowed >= 0) { 93 maxConsume = Math.max(maxConsume, allowed + sumOf(h2 + 1, totalDay)); 94 } 95 96 h2 = preIndex(2000, h2); 97 } 98 99 return sumUp[0] - maxConsume; 100 } 101 102 /** 103 * This function calculate a model that from day blockstart, we use enough bonus to feed the cost. 104 * And the day index is the only day before blockStart that use bonus, so how many bonus day index can use? 105 */ 106 int maxAllowedUse(int index, int blockStart) { 107 if (blockStart >= totalDay) { 108 return Math.min(remain[index], costs[index]); 109 } 110 return Math.min(Math.min(remain[blockStart] - (sumUp[blockStart] + costs[index] / 10), remain[index]), costs[index]); 111 } 112 113 /** 114 * A simple function to sum up costs[from], cost[from + 1], ... , costs[to -1] 115 */ 116 int sumOf(int from, int to) { 117 if (from >= to) { 118 return 0; 119 } 120 return sumUp[from] - sumUp[to]; 121 } 122 123 /** 124 * This function solve a problem, that all the day from index except day interval all use bouns, and all the day use bonus feed the cost other than day index. 125 * So how many bonus day index can use? 126 */ 127 int maxAllowedUseWithInterval(int index, int interval) { 128 if (interval >= totalDay) { 129 return Math.min(remain[index] - sumUp[index + 1], costs[index]); 130 } 131 return Math.min(Math.min(remain[index], remain[index] + costs[interval] / 10 - sumUp[interval + 1]) - sumOf(index + 1, interval), costs[index]); 132 } 133 134 int preIndex(int val, int cur) { 135 int i; 136 for (i = cur - 1; i >= 0 && costs[i] != val; i--) ; 137 return i; 138 } 139 140 /** 141 * @author dalt 142 * @see java.lang.AutoCloseable 143 * @since java1.7 144 */ 145 static class AcmInputReader implements AutoCloseable { 146 private PushbackInputStream in; 147 148 /** 149 * 创建读取器 150 * 151 * @param input 输入流 152 */ 153 public AcmInputReader(InputStream input) { 154 in = new PushbackInputStream(new BufferedInputStream(input)); 155 } 156 157 @Override 158 public void close() throws IOException { 159 in.close(); 160 } 161 162 private int nextByte() throws IOException { 163 return in.read() & 0xff; 164 } 165 166 /** 167 * 如果下一个字节为b,则跳过该字节 168 * 169 * @param b 被跳过的字节值 170 * @throws IOException if 输入流读取错误 171 */ 172 public void skipByte(int b) throws IOException { 173 int c; 174 if ((c = nextByte()) != b) { 175 in.unread(c); 176 } 177 } 178 179 /** 180 * 如果后续k个字节均为b,则跳过k个字节。这里{@literal k<times} 181 * 182 * @param b 被跳过的字节值 183 * @param times 跳过次数,-1表示无穷 184 * @throws IOException if 输入流读取错误 185 */ 186 public void skipByte(int b, int times) throws IOException { 187 int c; 188 while ((c = nextByte()) == b && times > 0) { 189 times--; 190 } 191 if (c != b) { 192 in.unread(c); 193 } 194 } 195 196 /** 197 * 类似于{@link #skipByte(int, int)}, 但是会跳过中间出现的空白字符。 198 * 199 * @param b 被跳过的字节值 200 * @param times 跳过次数,-1表示无穷 201 * @throws IOException if 输入流读取错误 202 */ 203 public void skipBlankAndByte(int b, int times) throws IOException { 204 int c; 205 skipBlank(); 206 while ((c = nextByte()) == b && times > 0) { 207 times--; 208 skipBlank(); 209 } 210 if (c != b) { 211 in.unread(c); 212 } 213 } 214 215 /** 216 * 读取下一块不含空白字符的字符块 217 * 218 * @return 下一块不含空白字符的字符块 219 * @throws IOException if 输入流读取错误 220 */ 221 public String nextBlock() throws IOException { 222 skipBlank(); 223 StringBuilder sb = new StringBuilder(); 224 int c = nextByte(); 225 while (AsciiMarksLazyHolder.asciiMarks[c = nextByte()] != AsciiMarksLazyHolder.BLANK_MARK) { 226 sb.append((char) c); 227 } 228 in.unread(c); 229 return sb.toString(); 230 } 231 232 /** 233 * 跳过输入流中后续空白字符 234 * 235 * @throws IOException if 输入流读取错误 236 */ 237 private void skipBlank() throws IOException { 238 int c; 239 while ((c = nextByte()) <= 32) ; 240 in.unread(c); 241 } 242 243 /** 244 * 读取下一个整数(可正可负),这里没有对溢出做判断 245 * 246 * @return 下一个整数值 247 * @throws IOException if 输入流读取错误 248 */ 249 public int nextInteger() throws IOException { 250 skipBlank(); 251 int value = 0; 252 boolean positive = true; 253 int c = nextByte(); 254 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { 255 positive = c == ‘+‘; 256 } else { 257 value = ‘0‘ - c; 258 } 259 c = nextByte(); 260 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 261 value = (value << 3) + (value << 1) + ‘0‘ - c; 262 c = nextByte(); 263 } 264 265 in.unread(c); 266 return positive ? -value : value; 267 } 268 269 /** 270 * 判断是否到了文件结尾 271 * 272 * @return true如果到了文件结尾,否则false 273 * @throws IOException if 输入流读取错误 274 */ 275 public boolean isMeetEOF() throws IOException { 276 int c = nextByte(); 277 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.EOF) { 278 return true; 279 } 280 in.unread(c); 281 return false; 282 } 283 284 /** 285 * 判断是否在跳过空白字符后抵达文件结尾 286 * 287 * @return true如果到了文件结尾,否则false 288 * @throws IOException if 输入流读取错误 289 */ 290 public boolean isMeetBlankAndEOF() throws IOException { 291 skipBlank(); 292 int c = nextByte(); 293 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.EOF) { 294 return true; 295 } 296 in.unread(c); 297 return false; 298 } 299 300 /** 301 * 获取下一个用英文字母组成的单词 302 * 303 * @return 下一个用英文字母组成的单词 304 */ 305 public String nextWord() throws IOException { 306 StringBuilder sb = new StringBuilder(16); 307 skipBlank(); 308 int c; 309 while ((AsciiMarksLazyHolder.asciiMarks[(c = nextByte())] & AsciiMarksLazyHolder.LETTER_MARK) != 0) { 310 sb.append((char) c); 311 } 312 in.unread(c); 313 return sb.toString(); 314 } 315 316 /** 317 * 读取下一个长整数(可正可负),这里没有对溢出做判断 318 * 319 * @return 下一个长整数值 320 * @throws IOException if 输入流读取错误 321 */ 322 public long nextLong() throws IOException { 323 skipBlank(); 324 long value = 0; 325 boolean positive = true; 326 int c = nextByte(); 327 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { 328 positive = c == ‘+‘; 329 } else { 330 value = ‘0‘ - c; 331 } 332 c = nextByte(); 333 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 334 value = (value << 3) + (value << 1) + ‘0‘ - c; 335 c = nextByte(); 336 } 337 in.unread(c); 338 return positive ? -value : value; 339 } 340 341 /** 342 * 读取下一个浮点数(可正可负),浮点数是近似值 343 * 344 * @return 下一个浮点数值 345 * @throws IOException if 输入流读取错误 346 */ 347 public float nextFloat() throws IOException { 348 return (float) nextDouble(); 349 } 350 351 /** 352 * 读取下一个浮点数(可正可负),浮点数是近似值 353 * 354 * @return 下一个浮点数值 355 * @throws IOException if 输入流读取错误 356 */ 357 public double nextDouble() throws IOException { 358 skipBlank(); 359 double value = 0; 360 boolean positive = true; 361 int c = nextByte(); 362 if (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.SIGN_MARK) { 363 positive = c == ‘+‘; 364 } else { 365 value = c - ‘0‘; 366 } 367 c = nextByte(); 368 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 369 value = value * 10.0 + c - ‘0‘; 370 c = nextByte(); 371 } 372 373 if (c == ‘.‘) { 374 double littlePart = 0; 375 double base = 1; 376 c = nextByte(); 377 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 378 littlePart = littlePart * 10.0 + c - ‘0‘; 379 base *= 10.0; 380 c = nextByte(); 381 } 382 value += littlePart / base; 383 } 384 in.unread(c); 385 return positive ? value : -value; 386 } 387 388 /** 389 * 读取下一个高精度数值 390 * 391 * @return 下一个高精度数值 392 * @throws IOException if 输入流读取错误 393 */ 394 public BigDecimal nextDecimal() throws IOException { 395 skipBlank(); 396 StringBuilder sb = new StringBuilder(); 397 sb.append((char) nextByte()); 398 int c = nextByte(); 399 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 400 sb.append((char) c); 401 c = nextByte(); 402 } 403 if (c == ‘.‘) { 404 sb.append(‘.‘); 405 c = nextByte(); 406 while (AsciiMarksLazyHolder.asciiMarks[c] == AsciiMarksLazyHolder.NUMERAL_MARK) { 407 sb.append((char) c); 408 c = nextByte(); 409 } 410 } 411 in.unread(c); 412 return new BigDecimal(sb.toString()); 413 } 414 415 private static class AsciiMarksLazyHolder { 416 public static final byte BLANK_MARK = 1; 417 public static final byte SIGN_MARK = 1 << 1; 418 public static final byte NUMERAL_MARK = 1 << 2; 419 public static final byte UPPERCASE_LETTER_MARK = 1 << 3; 420 public static final byte LOWERCASE_LETTER_MARK = 1 << 4; 421 public static final byte LETTER_MARK = UPPERCASE_LETTER_MARK | LOWERCASE_LETTER_MARK; 422 public static final byte EOF = 1 << 5; 423 public static byte[] asciiMarks = new byte[256]; 424 425 static { 426 for (int i = 0; i <= 32; i++) { 427 asciiMarks[i] = BLANK_MARK; 428 } 429 asciiMarks[‘+‘] = SIGN_MARK; 430 asciiMarks[‘-‘] = SIGN_MARK; 431 for (int i = ‘0‘; i <= ‘9‘; i++) { 432 asciiMarks[i] = NUMERAL_MARK; 433 } 434 for (int i = ‘a‘; i <= ‘z‘; i++) { 435 asciiMarks[i] = LOWERCASE_LETTER_MARK; 436 } 437 for (int i = ‘A‘; i <= ‘Z‘; i++) { 438 asciiMarks[i] = UPPERCASE_LETTER_MARK; 439 } 440 asciiMarks[0xff] = EOF; 441 } 442 } 443 } 444 }