方法 3 的代码 - 第 1 部分
格式化的代码对于单个答案来说太大了。将第 1 部分和第 2 部分加载到它们自己的模块中。
Option Explicit
' * Address of cell holding target value
' * Changes value if the target value is moved.
' * The code assumes both values are in the Source worksheet.
Const CellSrcTgt As String = "C2"
' * Column numbers within KeyValue table once
' * The leftmost column will always be 1 no matter what
' columns the KeyValue table occupies in the worksheet
' * Reverse values if the columns are swapped
Const ColKVKey As Long = 1
Const ColKVValue As Long = 2
' * Change values if the columns are swapped.
' * Increase ColRsltMax if a new column is added
' * Providing the table in the worksheet starts in column 1, column numbers
' are the same in the array and the worksheet. If the worksheet table
' does not start in column 1, two sets of column numbers constants will be
' required and all code referencing these constants will require review.
Const ColRsltTotal As Long = 1
Const ColRsltDiffAbs As Long = 2
Const ColRsltExpnKey As Long = 3
Const ColRsltExpnValue As Long = 4
Const ColRsltMax As Long = 4
' These specify the columns with the Pending array so the code is
' self-documenting. The Pending array is internal to this set of routine
' so there is no need to change theses values
Const ColPendExpn As Long = 1
Const ColPendDiff As Long = 2
Const ColPendMax As Long = 2
' * Change both of these constants if the KeyValue table
' does not start in column A of the worksheet
Const ColSrcKVFirst As String = "A"
Const ColSrcKVLast As String = "B"
' * Change both of these constants if the KeyValue table
' does not start in column A of the worksheet
' * Reverse values if the columns are swapped
Const ColSrcKVKey As String = "A"
Const ColSrcKVValue As String = "B"
' Defines the first row within the results worksheet of the range to which
' the Results array is written. Change if the number of header rows changes.
Const RowRsltWshtDataFirst As Long = 2
' Increase value if a second or third header row is added
' Reduce value to 1 if there is no header row
Const RowSrcDataFirst As Long = 2
' Change values to match your worksheet names
Const WshtRsltName As String = "Result"
Const WshSrcName As String = "Source"
' Variables used by more than one routine
' =======================================
' The KeyValue table will be loaded from the source worksheet to this
' variant as a 2D array
Dim KeyValue As Variant
'# ' Current row number for worksheet Diag
'# Dim RowDiagCrnt As Long
Sub Control3()
' Find the combinations of items from the KeyValue tables whose total values
' are closest to the target total.
'# Dim ExpnKeyCrnt As String
'# Dim ExpnValueCrnt As String
' While duplicating a pending row, its contents are held in these variable
Dim PendExpnCrnt As String
Dim PendDiffCrnt As Long
' * The Pending array hold information about combinations that are pending;
' that is, combinations that are on target or might become on target after
' addition of further items to the combination.
' * The array is redimensioned as a 2D array with 50,000 rows and 2 columns.
' Choice of 50,000 as the number of rows is arbitrary; less might be
' adequate and more might be better.
' * Typically with 2D arrays the first dimension is for columns and the
' second for rows so the number of rows can be increased or decreased with
' "ReDim Preserve". Arrays that are read from or are written to worksheets
' must have the columns and rows reversed. Pending is both written to and
' read from the worksheet Sort.
' * Column 1 holds detains of the combination as a string of the form
' "--+-+". The string has one "-" or "+" for every entry in the KeyValue
' table. If the Nth character in the string is "+", the Nth entry in the
' KeyValue table is included in the combination.
' * Column 2 holds TargetValue - TotalOfCombination.
Dim Pending() As Variant
Dim PosExpn As Long
' * Potential results are accumulated in this array.
' * The number of rows is defined by RowArrRsltsMax.
' * Initially every possible combination is added at the bottom of this
' array. Once the array is full, a new combination overwrites the
' previously stored combination with the worst total if the new combination
' has a better total. In this context, a better total is closer to the
' target total than a worse one.
' * Traditionally 2D arrays have columns as the first dimension and rows as
' the second dimension. Arrays to be written to a worksheet must have their
' dimensions the other way round. After each new result is added to this
' array, the array is written to the results rworksheet and the workbook
' saved. This slows the macro but means that if it is terminated with the
' Task Manager any results found are already saved to disc.
Dim Result() As Variant
Dim RowKVCrnt As Long ' Current row within KeyValue
Dim RowKVFirstPositive As Long ' First row within KeyValue with a +ve value
Dim RowPendCrnt As Long ' The current row in Pending
Dim RowPendCrntMax As Long ' The current last used row in Pending
Dim RowPendMaxMax As Long ' The last ever used row in Pending
' Defines the maximum number of results that will be accumulated
Const RowRsltArrMax As Long = 40
' Row in array Result to which the next result will be written providing
' RowArrRsltNext < RowArrRsltMax. Once RowArrRsltNext = RowArrRsltMax,
' any new combination overwrites an existing row.
Dim RowRsltArrNext As Long
' Control variable for For-Loop
Dim RowRsltArrCrnt As Long
' The last row of the KeyValue table within the source worksheet
Dim RowSrcDataLast As Long
' Used to calculate the duration of a run. Set by Timer to the number of
' seconds since midnight. The value includes fractions of a second but I
' cannot find any documentation that specifies how accurate the time is.
' I suspect it depends on the clock speed. Anyway, with OS and other
' background routines running at any time, no timings are that accurate.
Dim TimeStart As Double
Dim TotalNegative As Long ' The total of all negative values
Dim TotalPositive As Long ' The total of all posative values
Dim TotalTgt As Long ' The target value is copied from the source
' worksheet to this variable.
TimeStart = Timer
Application.DisplayStatusBar = True
Application.StatusBar = "No results found so far"
With Worksheets(WshSrcName)
' Find last row in KeyValue table
RowSrcDataLast = .Cells(Rows.Count, ColSrcKVKey).End(xlUp).Row
' Sort KeyValue table within worksheet by value
.Range(.Cells(RowSrcDataFirst, ColSrcKVKey), _
.Cells(RowSrcDataLast, ColSrcKVValue)) _
.Sort Key1:=.Range(ColSrcKVValue & RowSrcDataFirst), _
Order1:=xlAscending, Header:=xlNo, OrderCustom:=1, _
MatchCase:=False, Orientation:=xlTopToBottom, _
DataOption1:=xlSortNormal
' KeyValue is of data type Variant (meaning it can hold anything).
' This statement loads all the data from a range and places it in KeyValue
' as a 2D array. The first dimension will be for rows and the second for
' columns. Both lower bounds will be 1 regardless of where the range was
' located.
KeyValue = .Range(.Cells(RowSrcDataFirst, ColSrcKVFirst), _
.Cells(RowSrcDataLast, ColSrcKVLast)).Value
' Get the target value
TotalTgt = .Range(CellSrcTgt).Value
End With
' Gather information about the KeyValue table
TotalNegative = 0
For RowKVCrnt = 1 To UBound(KeyValue, 1)
If KeyValue(RowKVCrnt, ColKVValue) >= 0 Then
' Treat a value of zero as positive. Arbitrary choice.
Exit For
End If
TotalNegative = TotalNegative + KeyValue(RowKVCrnt, ColKVValue)
Next
RowKVFirstPositive = RowKVCrnt
TotalPositive = 0
For RowKVCrnt = RowKVCrnt To UBound(KeyValue, 1)
TotalPositive = TotalPositive + KeyValue(RowKVCrnt, ColKVValue)
Next
' Initialise result worksheet
With Worksheets(WshtRsltName)
.Cells.EntireRow.Delete
With .Cells(1, ColRsltTotal)
.Value = "Total"
.HorizontalAlignment = xlRight
End With
With .Cells(1, ColRsltDiffAbs)
.Value = "Abs diff"
.HorizontalAlignment = xlRight
End With
.Cells(1, ColRsltExpnKey) = "Key Expn"
.Cells(1, ColRsltExpnValue).Value = "Value Expn"
.Range(.Cells(1, 1), .Cells(1, ColRsltMax)).Font.Bold = True
.Columns(ColRsltTotal).NumberFormat = "#,##0"
.Columns(ColRsltDiffAbs).NumberFormat = "#,##0"
' This value will be overwritten if any combination gives an acceptable value
.Range("A2").Value = "No combinations found"
End With
RowRsltArrNext = 1
' The technique used does not require large amounts of memory for pending
' combinations. During testing the maximum number of rows used was 312 with
' RowRsltArrMax = 400.
ReDim Pending(1 To 1000, 1 To ColPendMax)
ReDim Result(1 To RowRsltArrMax, 1 To ColRsltMax)
' Seed Pending with one combination for every row in the
' KeyValue table with a positive value
RowPendCrntMax = 0
For RowKVCrnt = RowKVFirstPositive To UBound(KeyValue, 1)
RowPendCrntMax = RowPendCrntMax + 1
Pending(RowPendCrntMax, ColPendExpn) = String(RowKVCrnt - 1, "-") & "+" & _
String(UBound(KeyValue, 1) - RowKVCrnt, "-")
Pending(RowPendCrntMax, ColPendDiff) = TotalTgt - KeyValue(RowKVCrnt, ColKVValue)
Next
RowPendMaxMax = RowPendCrntMax
'# RowDiagCrnt = 1
'# With Worksheets("Diag")
'# .Cells.EntireRow.Delete
'# .Cells.ClearFormats
'# .Cells(RowDiagCrnt, 1).Value = "Pending"
'# With .Cells(RowDiagCrnt, 2)
'# .Value = "Index"
'# .HorizontalAlignment = xlRight
'# End With
'# .Cells(RowDiagCrnt, 3).Value = "Expn"
'# .Cells(RowDiagCrnt, 4).Value = "Key Expn"
'# .Cells(RowDiagCrnt, 5).Value = "Value Expn"
'# With .Cells(RowDiagCrnt, 6)
'# .Value = "Total"
'# .HorizontalAlignment = xlRight
'# End With
'# .Cells(RowDiagCrnt, 7).Value = "Diff"
'# RowDiagCrnt = RowDiagCrnt + 1
'# For RowPendCrnt = 1 To RowPendCrntMax
'# .Cells(RowDiagCrnt, 2).Value = RowPendCrnt
'# With .Cells(RowDiagCrnt, 3)
'# .Value = Pending(RowPendCrnt, ColPendExpn)
'# .Font.Name = "Courier New"
'# End With
'# Call GenExpn(Pending(RowPendCrnt, ColPendExpn), ExpnKeyCrnt, ExpnValueCrnt)
'# .Cells(RowDiagCrnt, 4).Value = ExpnKeyCrnt
'# .Cells(RowDiagCrnt, 5).Value = "'" & ExpnValueCrnt
'# .Cells(RowDiagCrnt, 6).Value = "=" & ExpnValueCrnt
'# With .Cells(RowDiagCrnt, 7)
'# .Value = Format(Pending(RowPendCrnt, ColPendDiff), "#,##0")
'# End With
'# RowDiagCrnt = RowDiagCrnt + 1
'# Next
'# End With
'# RowDiagCrnt = RowDiagCrnt + 1
Do While RowPendCrntMax > 0
' This combination may be one of those with a total nearest the target
If Not OutputRslt(Pending, RowPendCrntMax, Result, RowRsltArrNext) Then
' Result is full of results with a total equal to the target total.
' No point searching any more because there is no room for more results.
Application.DisplayStatusBar = False
Debug.Print "Max Pending=" & RowPendMaxMax
Debug.Print "Duration (sss.ss): " & Format(Timer - TimeStart, "#,##0.00")
TimeStart = Timer - TimeStart ' Duration
Debug.Print "Duration (m:ss): " & Format(TimeStart \ 60, "#,##0") & ":" & Format(TimeStart Mod 60, "00")
Call MsgBox("Result worksheet is full of on-target results.", vbOKOnly)
Exit Sub
End If
PendExpnCrnt = Pending(RowPendCrntMax, ColPendExpn)
PendDiffCrnt = Pending(RowPendCrntMax, ColPendDiff)
' Remove this combination from the Pending array.
' New copies will be added if appropriate.
RowPendCrntMax = RowPendCrntMax - 1
Select Case PendDiffCrnt
Case Is < 0
' * The current total for this row is above the target.
' * Create a new combination for every negative value that can be
' added.
' * Negative values can only be added after any existing negative
' values to avoid creating multiple copies of the same combination.
' * An expression is of the form "+--+--+" with the position of each
' "+" or "-" corresponding to a row in KeyValue
For PosExpn = RowKVFirstPositive - 1 To 1 Step -1
If Mid(PendExpnCrnt, PosExpn, 1) = "-" Then
' This negative value has not been added
RowPendCrntMax = RowPendCrntMax + 1
If PosExpn = 1 Then
' "+" replaces first "-"
Pending(RowPendCrntMax, ColPendExpn) = "+" & Mid(PendExpnCrnt, 2)
Else
' "+" replaces a "-" in the middle
Pending(RowPendCrntMax, ColPendExpn) = _
Mid(PendExpnCrnt, 1, PosExpn - 1) & _
"+" & _
Mid(PendExpnCrnt, PosExpn + 1)
End If
' KeyValue(RowKVCrnt, ColKVValue) is negative so subtracting it
' will increase PendDiffCrnt.
Pending(RowPendCrntMax, ColPendDiff) = _
PendDiffCrnt - KeyValue(PosExpn, ColKVValue)
Else
' This negative value is already within the combination
' so no more negative value can be added
Exit For
End If
Next
If RowPendMaxMax < RowPendCrntMax Then
RowPendMaxMax = RowPendCrntMax
End If
Case Is >= 0
' The current total for this row is equal to or below the target
' * Create a new combination for every positive value that can be
' added.
' * Positive values can only be added after any existing positive
' values to avoid creating multiple copies of the same combination.
' * An expression is of the form "+--+--+" with the position of each
' "+" or "-" corresponding to a row in KeyValue
For PosExpn = UBound(KeyValue, 1) To RowKVFirstPositive Step -1
If Mid(PendExpnCrnt, PosExpn, 1) = "-" Then
' This positive value has not been added
RowPendCrntMax = RowPendCrntMax + 1
If PosExpn = UBound(KeyValue, 1) Then
' "+" replaces final "-"
Pending(RowPendCrntMax, ColPendExpn) = Mid(PendExpnCrnt, 1, Len(PendExpnCrnt) - 1) & "+"
Else
' "+" replaces a "-" in the middle
Pending(RowPendCrntMax, ColPendExpn) = _
Mid(PendExpnCrnt, 1, PosExpn - 1) & _
"+" & _
Mid(PendExpnCrnt, PosExpn + 1)
End If
' KeyValue(RowKVCrnt, ColKVValue) is positive so subtracting it
' will reduce PendDiffCrnt.
Pending(RowPendCrntMax, ColPendDiff) = _
PendDiffCrnt - KeyValue(PosExpn, ColKVValue)
Else
' This positive value is already within the combination
' so no more positive value can be added
Exit For
End If
Next
If RowPendMaxMax < RowPendCrntMax Then
RowPendMaxMax = RowPendCrntMax
End If
End Select
'# With Worksheets("Diag")
'#
'# .Cells(RowDiagCrnt, 1).Value = "Result"
'# With .Cells(RowDiagCrnt, 2)
'# .Value = "Index"
'# .HorizontalAlignment = xlRight
'# End With
'# With .Cells(RowDiagCrnt, 3)
'# .Value = "Total"
'# .HorizontalAlignment = xlRight
'# End With
'# With .Cells(RowDiagCrnt, 4)
'# .Value = "Abs diff"
'# .HorizontalAlignment = xlRight
'# End With
'# .Cells(RowDiagCrnt, 5).Value = "Key Expn"
'# .Cells(RowDiagCrnt, 6).Value = "Value Expn"
'# RowDiagCrnt = RowDiagCrnt + 1
'# For RowRsltArrCrnt = 1 To UBound(Result, 1)
'# If RowRsltArrCrnt < RowRsltArrNext Then
'# .Cells(RowDiagCrnt, 2).Value = RowRsltArrCrnt
'# With .Cells(RowDiagCrnt, 3)
'# .Value = Result(RowRsltArrCrnt, ColRsltTotal)
'# .NumberFormat = "#,##0"
'# End With
'# With .Cells(RowDiagCrnt, 4)
'# .Value = Result(RowRsltArrCrnt, ColRsltDiffAbs)
'# .NumberFormat = "#,##0"
'# End With
'# .Cells(RowDiagCrnt, 5).Value = Result(RowRsltArrCrnt, ColRsltExpnKey)
'# .Cells(RowDiagCrnt, 6).Value = Result(RowRsltArrCrnt, ColRsltExpnValue)
'# RowDiagCrnt = RowDiagCrnt + 1
'# End If
'# Next
'#
'# .Cells(RowDiagCrnt, 1).Value = "Pending"
'# With .Cells(RowDiagCrnt, 2)
'# .Value = "Index"
'# .HorizontalAlignment = xlRight
'# End With
'# .Cells(RowDiagCrnt, 3).Value = "Expn"
'# .Cells(RowDiagCrnt, 4).Value = "Key Expn"
'# .Cells(RowDiagCrnt, 5).Value = "Value Expn"
'# With .Cells(RowDiagCrnt, 6)
'# .Value = "Total"
'# .HorizontalAlignment = xlRight
'# End With
'# .Cells(RowDiagCrnt, 7).Value = "Diff"
'# RowDiagCrnt = RowDiagCrnt + 1
'# For RowPendCrnt = 1 To RowPendCrntMax
'# .Cells(RowDiagCrnt, 2).Value = RowPendCrnt
'# With .Cells(RowDiagCrnt, 3)
'# .Value = Pending(RowPendCrnt, ColPendExpn)
'# .Font.Name = "Courier New"
'# End With
'# Call GenExpn(Pending(RowPendCrnt, ColPendExpn), ExpnKeyCrnt, ExpnValueCrnt)
'# .Cells(RowDiagCrnt, 4).Value = ExpnKeyCrnt
'# .Cells(RowDiagCrnt, 5).Value = "'" & ExpnValueCrnt
'# .Cells(RowDiagCrnt, 6).Value = "=" & ExpnValueCrnt
'# With .Cells(RowDiagCrnt, 7)
'# .Value = Format(Pending(RowPendCrnt, ColPendDiff), "#,##0")
'# End With
'# RowDiagCrnt = RowDiagCrnt + 1
'# Next
'#
'# End With
'# RowDiagCrnt = RowDiagCrnt + 1
Loop ' While RowPendCrntMax > 0
' Will only fall out the bottom of the loop if Result array not full of on-target
' results. Final version of Result array will not have been written to worksheet
'# With Worksheets("Diag")
'# .Columns("A:" & ColNumToCode(UBound(Result, 2) + 2)).AutoFit
'# End With
With Worksheets(WshtRsltName)
.Range(.Cells(RowRsltWshtDataFirst, 1), _
.Cells(RowRsltWshtDataFirst + UBound(Result, 1) - 1, _
UBound(Result, 2))) = Result
.Columns("A:" & ColNumToCode(UBound(Result, 2))).AutoFit
End With
ThisWorkbook.Save
Application.DisplayStatusBar = False
Debug.Print "Max Pending=" & RowPendMaxMax
Debug.Print "Duration (sss.ss): " & Format(Timer - TimeStart, "#,##0.00")
TimeStart = Timer - TimeStart
Debug.Print "Duration (m:ss): " & Format(TimeStart \ 60, "#,##0") & ":" & Format(TimeStart Mod 60, "00")
End Sub